11
11
import time
12
12
from pathlib import Path
13
13
14
- from typing import Dict , Iterable , Iterator , List , Optional , Tuple
14
+ from typing import Any , Dict , Iterable , Iterator , List , Optional , Tuple
15
15
16
16
17
17
class ShellSession :
@@ -116,17 +116,21 @@ class ProjectToTest:
116
116
117
117
# Where can we clone the project from?
118
118
git_url : Optional [str ] = None
119
+ slug : Optional [str ] = None
119
120
120
121
def __init__ (self ):
121
- if self .git_url :
122
- self . slug = self .git_url . split ( "/" )[ - 1 ]
123
- self .dir = Path ( self .slug )
122
+ if not self .slug :
123
+ if self .git_url :
124
+ self .slug = self .git_url . split ( "/" )[ - 1 ]
124
125
125
- def get_source (self , shell ):
126
- """Get the source of the project."""
126
+ def make_dir (self ):
127
+ self . dir = Path ( f"work_ { self . slug } " )
127
128
if self .dir .exists ():
128
129
rmrf (self .dir )
129
- shell .run_command (f"git clone { self .git_url } " )
130
+
131
+ def get_source (self , shell ):
132
+ """Get the source of the project."""
133
+ shell .run_command (f"git clone { self .git_url } { self .dir } " )
130
134
131
135
def prep_environment (self , env ):
132
136
"""Prepare the environment to run the test suite.
@@ -135,20 +139,41 @@ def prep_environment(self, env):
135
139
"""
136
140
pass
137
141
142
+ def tweak_coverage_settings (self , settings : Iterable [Tuple [str , Any ]]) -> Iterator [None ]:
143
+ """Tweak the coverage settings.
144
+
145
+ NOTE: This is not properly factored, and is only used by ToxProject now!!!
146
+ """
147
+ pass
148
+
138
149
def run_no_coverage (self , env ):
139
150
"""Run the test suite with no coverage measurement."""
140
151
pass
141
152
142
- def run_with_coverage (self , env , pip_args , cov_options ):
153
+ def run_with_coverage (self , env , pip_args , cov_tweaks ):
143
154
"""Run the test suite with coverage measurement."""
144
155
pass
145
156
146
157
158
+ class EmptyProject (ProjectToTest ):
159
+ """A dummy project for testing other parts of this code."""
160
+ def __init__ (self , slug : str = "empty" , fake_durations : Iterable [float ]= (1.23 ,)):
161
+ self .slug = slug
162
+ self .durations = iter (itertools .cycle (fake_durations ))
163
+
164
+ def get_source (self , shell ):
165
+ pass
166
+
167
+ def run_with_coverage (self , env , pip_args , cov_tweaks ):
168
+ """Run the test suite with coverage measurement."""
169
+ return next (self .durations )
170
+
171
+
147
172
class ToxProject (ProjectToTest ):
148
173
"""A project using tox to run the test suite."""
149
174
150
175
def prep_environment (self , env ):
151
- env .shell .run_command (f"{ env .python } -m pip install tox" )
176
+ env .shell .run_command (f"{ env .python } -m pip install ' tox<4' " )
152
177
self .run_tox (env , env .pyver .toxenv , "--notest" )
153
178
154
179
def run_tox (self , env , toxenv , toxargs = "" ):
@@ -159,26 +184,30 @@ def run_tox(self, env, toxenv, toxargs=""):
159
184
def run_no_coverage (self , env ):
160
185
return self .run_tox (env , env .pyver .toxenv , "--skip-pkg-install" )
161
186
162
- def run_with_coverage (self , env , pip_args , cov_options ):
163
- assert not cov_options , f"ToxProject.run_with_coverage can't take cov_options={ cov_options !r} "
187
+ def run_with_coverage (self , env , pip_args , cov_tweaks ):
164
188
self .run_tox (env , env .pyver .toxenv , "--notest" )
165
189
env .shell .run_command (
166
190
f".tox/{ env .pyver .toxenv } /bin/python -m pip install { pip_args } "
167
191
)
168
- return self .run_tox (env , env .pyver .toxenv , "--skip-pkg-install" )
192
+ with self .tweak_coverage_settings (cov_tweaks ):
193
+ self .pre_check (env ) # NOTE: Not properly factored, and only used from here.
194
+ duration = self .run_tox (env , env .pyver .toxenv , "--skip-pkg-install" )
195
+ self .post_check (env ) # NOTE: Not properly factored, and only used from here.
196
+ return duration
169
197
170
198
171
199
class ProjectPytestHtml (ToxProject ):
172
200
"""pytest-dev/pytest-html"""
173
201
174
202
git_url = "https://github.com/pytest-dev/pytest-html"
175
203
176
- def run_with_coverage (self , env , pip_args , cov_options ):
204
+ def run_with_coverage (self , env , pip_args , cov_tweaks ):
205
+ raise Exception ("This doesn't work because options changed to tweaks" )
177
206
covenv = env .pyver .toxenv + "-cov"
178
207
self .run_tox (env , covenv , "--notest" )
179
208
env .shell .run_command (f".tox/{ covenv } /bin/python -m pip install { pip_args } " )
180
- if cov_options :
181
- replace = ("# reference: https" , f"[run]\n { cov_options } \n #" )
209
+ if cov_tweaks :
210
+ replace = ("# reference: https" , f"[run]\n { cov_tweaks } \n #" )
182
211
else :
183
212
replace = ("" , "" )
184
213
with file_replace (Path (".coveragerc" ), * replace ):
@@ -206,6 +235,34 @@ class ProjectAttrs(ToxProject):
206
235
207
236
git_url = "https://github.com/python-attrs/attrs"
208
237
238
+ def tweak_coverage_settings (self , tweaks : Iterable [Tuple [str , Any ]]) -> Iterator [None ]:
239
+ return tweak_toml_coverage_settings ("pyproject.toml" , tweaks )
240
+
241
+ def pre_check (self , env ):
242
+ env .shell .run_command ("cat pyproject.toml" )
243
+
244
+ def post_check (self , env ):
245
+ env .shell .run_command ("ls -al" )
246
+
247
+
248
+ def tweak_toml_coverage_settings (toml_file : str , tweaks : Iterable [Tuple [str , Any ]]) -> Iterator [None ]:
249
+ if tweaks :
250
+ toml_inserts = []
251
+ for name , value in tweaks :
252
+ if isinstance (value , bool ):
253
+ toml_inserts .append (f"{ name } = { str (value ).lower ()} " )
254
+ elif isinstance (value , str ):
255
+ toml_inserts .append (f"{ name } = '{ value } '" )
256
+ else :
257
+ raise Exception (f"Can't tweak toml setting: { name } = { value !r} " )
258
+ header = "[tool.coverage.run]\n "
259
+ insert = header + "\n " .join (toml_inserts ) + "\n "
260
+ else :
261
+ header = insert = ""
262
+ return file_replace (Path (toml_file ), header , insert )
263
+
264
+
265
+
209
266
210
267
class AdHocProject (ProjectToTest ):
211
268
"""A standalone program to run locally."""
@@ -232,7 +289,7 @@ def run_no_coverage(self, env):
232
289
env .shell .run_command (f"{ env .python } { self .python_file } " )
233
290
return env .shell .last_duration
234
291
235
- def run_with_coverage (self , env , pip_args , cov_options ):
292
+ def run_with_coverage (self , env , pip_args , cov_tweaks ):
236
293
env .shell .run_command (f"{ env .python } -m pip install { pip_args } " )
237
294
with change_dir (self .cur_dir ):
238
295
env .shell .run_command (
@@ -265,15 +322,13 @@ class PyVersion:
265
322
# The tox environment to run this Python
266
323
toxenv : str
267
324
268
-
269
325
class Python (PyVersion ):
270
326
"""A version of CPython to use."""
271
327
272
328
def __init__ (self , major , minor ):
273
329
self .command = self .slug = f"python{ major } .{ minor } "
274
330
self .toxenv = f"py{ major } { minor } "
275
331
276
-
277
332
class PyPy (PyVersion ):
278
333
"""A version of PyPy to use."""
279
334
@@ -288,6 +343,7 @@ def __init__(self, path, slug):
288
343
self .slug = slug
289
344
self .toxenv = None
290
345
346
+
291
347
@dataclasses .dataclass
292
348
class Coverage :
293
349
"""A version of coverage.py to use, maybe None."""
@@ -296,33 +352,33 @@ class Coverage:
296
352
# Arguments for "pip install ..."
297
353
pip_args : Optional [str ] = None
298
354
# Tweaks to the .coveragerc file
299
- options : Optional [str ] = None
355
+ tweaks : Optional [Iterable [ Tuple [ str , Any ]] ] = None
300
356
301
357
class CoveragePR (Coverage ):
302
358
"""A version of coverage.py from a pull request."""
303
- def __init__ (self , number , options = None ):
359
+ def __init__ (self , number , tweaks = None ):
304
360
super ().__init__ (
305
361
slug = f"#{ number } " ,
306
362
pip_args = f"git+https://github.com/nedbat/coveragepy.git@refs/pull/{ number } /merge" ,
307
- options = options ,
363
+ tweaks = tweaks ,
308
364
)
309
365
310
366
class CoverageCommit (Coverage ):
311
367
"""A version of coverage.py from a specific commit."""
312
- def __init__ (self , sha , options = None ):
368
+ def __init__ (self , sha , tweaks = None ):
313
369
super ().__init__ (
314
370
slug = sha ,
315
371
pip_args = f"git+https://github.com/nedbat/coveragepy.git@{ sha } " ,
316
- options = options ,
372
+ tweaks = tweaks ,
317
373
)
318
374
319
375
class CoverageSource (Coverage ):
320
376
"""The coverage.py in a working tree."""
321
- def __init__ (self , directory , options = None ):
377
+ def __init__ (self , directory , tweaks = None ):
322
378
super ().__init__ (
323
379
slug = "source" ,
324
380
pip_args = directory ,
325
- options = options ,
381
+ tweaks = tweaks ,
326
382
)
327
383
328
384
@@ -337,6 +393,8 @@ class Env:
337
393
338
394
ResultData = Dict [Tuple [str , str , str ], float ]
339
395
396
+ DIMENSION_NAMES = ["proj" , "pyver" , "cov" ]
397
+
340
398
class Experiment :
341
399
"""A particular time experiment to run."""
342
400
@@ -353,9 +411,18 @@ def __init__(
353
411
354
412
def run (self , num_runs : int = 3 ) -> None :
355
413
results = []
414
+ total_runs = (
415
+ len (self .projects ) *
416
+ len (self .py_versions ) *
417
+ len (self .cov_versions ) *
418
+ num_runs
419
+ )
420
+ total_run_nums = iter (itertools .count (start = 1 ))
421
+
356
422
for proj in self .projects :
357
423
print (f"Testing with { proj .slug } " )
358
424
with ShellSession (f"output_{ proj .slug } .log" ) as shell :
425
+ proj .make_dir ()
359
426
proj .get_source (shell )
360
427
361
428
for pyver in self .py_versions :
@@ -366,20 +433,23 @@ def run(self, num_runs: int = 3) -> None:
366
433
shell .run_command (f"{ python } -V" )
367
434
env = Env (pyver , python , shell )
368
435
369
- with change_dir (Path ( proj .slug ) ):
436
+ with change_dir (proj .dir ):
370
437
print (f"Prepping for { proj .slug } { pyver .slug } " )
371
438
proj .prep_environment (env )
372
439
for cov_ver in self .cov_versions :
373
440
durations = []
374
441
for run_num in range (num_runs ):
442
+ total_run_num = next (total_run_nums )
375
443
print (
376
- f"Running tests, cov={ cov_ver .slug } , { run_num + 1 } of { num_runs } "
444
+ f"Running tests, cov={ cov_ver .slug } , " +
445
+ f"{ run_num + 1 } of { num_runs } , " +
446
+ f"total { total_run_num } /{ total_runs } "
377
447
)
378
448
if cov_ver .pip_args is None :
379
449
dur = proj .run_no_coverage (env )
380
450
else :
381
451
dur = proj .run_with_coverage (
382
- env , cov_ver .pip_args , cov_ver .options ,
452
+ env , cov_ver .pip_args , cov_ver .tweaks ,
383
453
)
384
454
print (f"Tests took { dur :.3f} s" )
385
455
durations .append (dur )
@@ -411,7 +481,7 @@ def show_results(
411
481
412
482
table_axes = [dimensions [rowname ] for rowname in rows ]
413
483
data_order = [* rows , column ]
414
- remap = [data_order .index (datum ) for datum in [ "proj" , "pyver" , "cov" ] ]
484
+ remap = [data_order .index (datum ) for datum in DIMENSION_NAMES ]
415
485
416
486
WIDTH = 20
417
487
def as_table_row (vals ):
@@ -445,8 +515,12 @@ def as_table_row(vals):
445
515
PERF_DIR = Path ("/tmp/covperf" )
446
516
447
517
def run_experiment (
448
- py_versions : List [PyVersion ], cov_versions : List [Coverage ], projects : List [ProjectToTest ],
449
- rows : List [str ], column : str , ratios : Iterable [Tuple [str , str , str ]] = (),
518
+ py_versions : List [PyVersion ],
519
+ cov_versions : List [Coverage ],
520
+ projects : List [ProjectToTest ],
521
+ rows : List [str ],
522
+ column : str ,
523
+ ratios : Iterable [Tuple [str , str , str ]] = (),
450
524
):
451
525
slugs = [v .slug for v in py_versions + cov_versions + projects ]
452
526
if len (set (slugs )) != len (slugs ):
@@ -456,6 +530,8 @@ def run_experiment(
456
530
ratio_slugs = [rslug for ratio in ratios for rslug in ratio [1 :]]
457
531
if any (rslug not in slugs for rslug in ratio_slugs ):
458
532
raise Exception (f"Ratio slug doesn't match a slug: { ratio_slugs } , { slugs } " )
533
+ if set (rows + [column ]) != set (DIMENSION_NAMES ):
534
+ raise Exception (f"All of these must be in rows or column: { ', ' .join (DIMENSION_NAMES )} " )
459
535
460
536
print (f"Removing and re-making { PERF_DIR } " )
461
537
rmrf (PERF_DIR )
@@ -466,7 +542,7 @@ def run_experiment(
466
542
exp .show_results (rows = rows , column = column , ratios = ratios )
467
543
468
544
469
- if 1 :
545
+ if 0 :
470
546
run_experiment (
471
547
py_versions = [
472
548
#Python(3, 11),
@@ -489,3 +565,30 @@ def run_experiment(
489
565
("94231 vs 3.10" , "94231" , "v3.10.5" ),
490
566
],
491
567
)
568
+
569
+
570
+ if 1 :
571
+ run_experiment (
572
+ py_versions = [
573
+ Python (3 , 11 ),
574
+ ],
575
+ cov_versions = [
576
+ Coverage ("701" , "coverage==7.0.1" ),
577
+ Coverage ("701.dynctx" , "coverage==7.0.1" , [("dynamic_context" , "test_function" )]),
578
+ Coverage ("702" , "coverage==7.0.2" ),
579
+ Coverage ("702.dynctx" , "coverage==7.0.2" , [("dynamic_context" , "test_function" )]),
580
+ ],
581
+ projects = [
582
+ #EmptyProject("empty", [1.2, 3.4]),
583
+ #EmptyProject("dummy", [6.9, 7.1]),
584
+ #ProjectDateutil(),
585
+ ProjectAttrs (),
586
+ ],
587
+ rows = ["proj" , "pyver" ],
588
+ column = "cov" ,
589
+ ratios = [
590
+ (".2 vs .1" , "702" , "701" ),
591
+ (".1 dynctx cost" , "701.dynctx" , "701" ),
592
+ (".2 dynctx cost" , "702.dynctx" , "702" ),
593
+ ],
594
+ )
0 commit comments