forked from cocotb/cocotb
-
Notifications
You must be signed in to change notification settings - Fork 0
/
noxfile.py
609 lines (488 loc) · 18.7 KB
/
noxfile.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
# Copyright cocotb contributors
# Licensed under the Revised BSD License, see LICENSE for details.
# SPDX-License-Identifier: BSD-3-Clause
import glob
import os
import shutil
import sys
from contextlib import suppress
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import nox
# Sessions run by default if nox is called without further arguments.
nox.options.sessions = ["dev_test"]
test_deps = ["pytest>=6"]
coverage_deps = ["coverage[toml]>=5.0", "pytest-cov"]
# gcovr 5.1 has an issue parsing some gcov files, so pin to 5.0. See
# https://github.com/gcovr/gcovr/issues/596
# When using gcovr 5.0, deprecated jinja2.Markup was removed in 3.1, so an
# Exception is raised during html report generation.
# See https://github.com/gcovr/gcovr/pull/576
# These issues are fixed on gcovr master branch, so next release should work.
coverage_report_deps = ["coverage[toml]>=5.0", "jinja2<3.1", "gcovr==5.0"]
dev_deps = [
"mypy",
"pre-commit",
"nox",
"ruff",
"clang-format",
]
# Version of the cibuildwheel package used to build wheels.
cibuildwheel_version = "2.15.0"
#
# Helpers for use within this file.
#
def simulator_support_matrix() -> List[Tuple[str, str, str]]:
"""
Get a list of supported simulator/toplevel-language/GPI-interface tuples.
"""
# Simulators with support for VHDL through VHPI, and Verilog through VPI.
standard = [
(sim, toplevel_lang, gpi_interface)
for sim in ("activehdl", "riviera", "xcelium")
for toplevel_lang in ("verilog", "vhdl")
for gpi_interface in ("vpi", "vhpi")
if (toplevel_lang, gpi_interface) in (("verilog", "vpi"), ("vhdl", "vhpi"))
]
# Special-case simulators.
special = [
("cvc", "verilog", "vpi"),
("ghdl", "vhdl", "vpi"),
("icarus", "verilog", "vpi"),
("nvc", "vhdl", "vhpi"),
("questa", "verilog", "vpi"),
("questa", "vhdl", "fli"),
("questa", "vhdl", "vhpi"),
("verilator", "verilog", "vpi"),
("vcs", "verilog", "vpi"),
]
return standard + special
def env_vars_for_test(
sim: Optional[str], toplevel_lang: Optional[str], gpi_interface: Optional[str]
) -> Dict[str, str]:
"""Prepare the environment variables controlling the test run."""
e = {}
if sim is not None:
e["SIM"] = sim
if os.getenv("TOPLEVEL_LANG") is not None:
e["HDL_TOPLEVEL_LANG"] = os.getenv("TOPLEVEL_LANG")
if toplevel_lang is not None:
e["TOPLEVEL_LANG"] = toplevel_lang
e["HDL_TOPLEVEL_LANG"] = toplevel_lang
assert not (toplevel_lang == "verilog" and gpi_interface != "vpi")
if toplevel_lang == "vhdl" and gpi_interface is not None:
e["VHDL_GPI_INTERFACE"] = gpi_interface
return e
def stringify_dict(d: Dict[str, str]) -> str:
return ", ".join(f"{k}={v}" for k, v in d.items())
def configure_env_for_dev_build(session: nox.session) -> None:
"""Set environment variables for a development build.
- Enable coverage collection.
- Build with more aggressive error checking.
"""
session.env["CFLAGS"] = " ".join(
[
"-Werror",
"-Wno-deprecated-declarations",
"-Wsuggest-override",
"-g",
"--coverage",
]
)
session.env["COCOTB_LIBRARY_COVERAGE"] = "1"
session.env["CXXFLAGS"] = "-Werror"
session.env["LDFLAGS"] = "--coverage"
#
# Development pipeline
#
# - Use nox to build an sdist; no separate build step is required.
# - Run tests against the installed sdist.
# - Collect coverage.
#
@nox.session
def dev_build(session: nox.Session) -> None:
session.warn("No building is necessary for development sessions.")
@nox.session
def dev_test(session: nox.Session) -> None:
"""Run all development tests as configured through environment variables."""
dev_test_sim(session, sim=None, toplevel_lang=None, gpi_interface=None)
dev_test_nosim(session)
dev_coverage_combine(session)
@nox.session
@nox.parametrize("sim,toplevel_lang,gpi_interface", simulator_support_matrix())
def dev_test_sim(
session: nox.Session,
sim: Optional[str],
toplevel_lang: Optional[str],
gpi_interface: Optional[str],
) -> None:
"""Test a development version of cocotb against a simulator."""
configure_env_for_dev_build(session)
session.run("pip", "install", *test_deps, *coverage_deps)
# Editable installs break C/C++ coverage collection; don't use them.
# C/C++ coverage collection requires that the object files produced by the
# compiler are not moved around, otherwise the gcno and gcda files produced
# at compile and runtime, respectively, are located in the wrong
# directories. Depending on the version of the Python install machinery
# editable builds are done in a directory in /tmp, which is removed after
# the build completes, taking all gcno files with them, as well as the path
# to place the gcda files.
session.run("pip", "install", ".")
env = env_vars_for_test(sim, toplevel_lang, gpi_interface)
config_str = stringify_dict(env)
# Remove a potentially existing coverage file from a previous run for the
# same test configuration. Use a filename *not* starting with `.coverage.`,
# as coverage.py assumes ownership over these files and deleted them at
# will.
coverage_file = Path(f".cov.test.sim-{sim}-{toplevel_lang}-{gpi_interface}")
with suppress(FileNotFoundError):
coverage_file.unlink()
session.log(f"Running 'make test' against a simulator {config_str}")
session.run("make", "test", external=True, env=env)
session.log(f"Running simulator-specific tests against a simulator {config_str}")
session.run(
"pytest",
"-v",
"--cov=cocotb",
"--cov-branch",
# Don't display coverage report here
"--cov-report=",
"-k",
"simulator_required",
env=env,
)
Path(".coverage").rename(".coverage.pytest")
session.log(f"Running examples against a simulator {config_str}")
pytest_example_tree = [
"examples/adder",
"examples/simple_dff",
"examples/matrix_multiplier",
"examples/mixed_language",
]
session.run(
"pytest",
"-v",
*pytest_example_tree,
env=env,
)
session.log(f"All tests and examples passed with configuration {config_str}!")
# Combine coverage produced during the test runs, and place it in a file
# with a name specific to this invocation of dev_test_sim().
coverage_files = glob.glob("**/.coverage.cocotb", recursive=True)
if not coverage_files:
session.error(
"No coverage files found. Something went wrong during the test execution."
)
coverage_files.append(".coverage.pytest")
session.run("coverage", "combine", "--append", *coverage_files)
Path(".coverage").rename(coverage_file)
session.log(f"Stored Python coverage for this test run in {coverage_file}.")
@nox.session
def dev_test_nosim(session: nox.Session) -> None:
"""Run the simulator-agnostic tests against a cocotb development version."""
configure_env_for_dev_build(session)
session.run("pip", "install", *test_deps, *coverage_deps)
session.run("pip", "install", ".")
# Remove a potentially existing coverage file from a previous run for the
# same test configuration. Use a filename *not* starting with `.coverage.`,
# as coverage.py assumes ownership over these files and deleted them at
# will.
coverage_file = Path(".cov.test.nosim")
with suppress(FileNotFoundError):
coverage_file.unlink()
# Run pytest with the default configuration in setup.cfg.
session.log("Running simulator-agnostic tests with pytest")
session.run(
"pytest",
"-v",
"--cov=cocotb",
"--cov-branch",
# Don't display coverage report here
"--cov-report=",
"-k",
"not simulator_required",
)
# Run pytest for files which can only be tested in the source tree, not in
# the installed binary (otherwise we get an "import file mismatch" error
# from pytest).
#
# The following tests are disabled because they do not work without an
# editable cocotb installation:
# "ERROR cocotb/_sim_versions.py - ImportError: cannot import name
# 'simulator' from partially initialized module 'cocotb' (most likely due to
# a circular import) (/home/runner/work/cocotb/cocotb/cocotb/__init__.py)"
# TODO: Re-enable once we have sorted out the import issues.
#
# session.log("Running simulator-agnostic tests in the source tree with pytest")
# pytest_sourcetree = [
# "cocotb/utils.py",
# "cocotb/binary.py",
# "cocotb/types/",
# "cocotb/_sim_versions.py",
# ]
# session.run(
# "pytest",
# "-v",
# "--doctest-modules",
# "--cov=cocotb",
# "--cov-branch",
# # Don't display coverage report here
# "--cov-report=",
# # Append to the .coverage file created in the previous pytest
# # invocation in this session.
# "--cov-append",
# "-k",
# "not simulator_required",
# *pytest_sourcetree,
# )
session.log("All tests passed!")
# Rename the .coverage file to make it unique to the session.
Path(".coverage").rename(coverage_file)
session.log(f"Stored Python coverage for this test run in {coverage_file}.")
@nox.session
def dev_coverage_combine(session: nox.Session) -> None:
"""Combine coverage from previous dev_* runs into a .coverage file."""
session.run("pip", "install", *coverage_report_deps)
coverage_files = glob.glob("**/.cov.test.*", recursive=True)
session.run("coverage", "combine", *coverage_files)
assert Path(".coverage").is_file()
session.log("Wrote combined coverage database for all tests to '.coverage'.")
session.notify("dev_coverage_report")
@nox.session
def dev_coverage_report(session: nox.Session) -> None:
"""Report coverage results."""
session.run("pip", "install", *coverage_report_deps)
# Produce Cobertura XML coverage reports.
session.log("Producing Python and C/C++ coverage in Cobertura XML format")
coverage_python_xml = Path(".python_coverage.xml")
session.run("coverage", "xml", "-o", str(coverage_python_xml))
assert coverage_python_xml.is_file()
coverage_cpp_xml = Path(".cpp_coverage.xml")
session.run(
"gcovr",
"--xml",
"--output",
str(coverage_cpp_xml),
".",
)
assert coverage_cpp_xml.is_file()
session.log(
f"Cobertura XML files written to {str(coverage_cpp_xml)!r} (C/C++) and {str(coverage_python_xml)!r} (Python)"
)
# Report human-readable coverage.
session.log("Python coverage")
session.run("coverage", "report")
session.log("Library coverage")
session.run("gcovr", "--print-summary", "--txt")
#
# Release pipeline.
#
# - Clean out the dist directory.
# - Build wheels (release builds).
# - Install cocotb from wheel.
# - Run tests against cocotb installed from the wheel.
#
# The release pipeline does not collect coverage, and does not run doctests.
#
# Directory containing the distribution artifacts (sdist and bdist).
dist_dir = "dist"
@nox.session
def release_clean(session: nox.Session) -> None:
"""Remove all build artifacts from the dist directory."""
shutil.rmtree(dist_dir, ignore_errors=True)
@nox.session
def release_build(session: nox.Session) -> None:
"""Build a release (sdist and bdist)."""
session.notify("release_build_bdist")
session.notify("release_build_sdist")
@nox.session
def release_build_bdist(session: nox.Session) -> None:
"""Build a binary distribution (wheels) on the current operating system."""
# Pin a version to ensure reproducible builds.
session.run("pip", "install", f"cibuildwheel=={cibuildwheel_version}")
# cibuildwheel only auto-detects the platform if it runs on a CI server.
# Do the auto-detect manually to enable local runs.
if sys.platform.startswith("linux"):
platform = "linux"
elif sys.platform == "darwin":
platform = "macos"
elif sys.platform == "win32":
platform = "windows"
else:
session.error(f"Unknown platform: {sys.platform!r}")
session.log("Building binary distribution (wheels)")
session.run(
"cibuildwheel",
"--platform",
platform,
"--output-dir",
dist_dir,
)
session.log(
f"Binary distribution in release mode for {platform!r} built into {dist_dir!r}"
)
@nox.session
def release_build_sdist(session: nox.Session) -> None:
"""Build the source distribution."""
session.run("pip", "install", "build")
session.log("Building source distribution (sdist)")
session.run("python", "-m", "build", "--sdist", "--outdir", dist_dir, ".")
session.log(f"Source distribution in release mode built into {dist_dir!r}")
@nox.session
def release_test_sdist(session: nox.Session) -> None:
"""Build and install the sdist."""
# Find the sdist to install.
sdists = list(Path(dist_dir).glob("cocotb-*.tar.gz"))
if len(sdists) == 0:
session.error(
f"No *.tar.gz sdist file found in {dist_dir!r} "
f"Run the 'release_build' session first."
)
if len(sdists) > 1:
session.error(
f"More than one potential sdist found in the {dist_dir!r} "
f"directory. Run the 'release_clean' session first!"
)
sdist_path = sdists[0]
assert sdist_path.is_file()
session.log("Installing cocotb from sdist, which includes the build step")
session.run(
"pip",
"install",
str(sdist_path),
)
session.log("Running cocotb-config as basic installation smoke test")
session.run("cocotb-config", "--version")
def release_install(session: nox.Session) -> None:
"""Helper: Install cocotb from wheels and also install test dependencies."""
# We have to disable the use of the PyPi index when installing cocotb to
# guarantee that the wheels in dist are being used. But without an index
# pip cannot find the dependencies, which need to be installed from PyPi.
# Work around that by explicitly installing the dependencies first from
# PyPi, and then installing cocotb itself from the local dist directory.
session.log("Installing cocotb dependencies from PyPi")
session.run("pip", "install", "find_libpython")
session.log(f"Installing cocotb from wheels in {dist_dir!r}")
session.run(
"pip",
"install",
"--force-reinstall",
"--only-binary",
"cocotb",
"--no-index",
"--no-dependencies",
"--find-links",
dist_dir,
"cocotb",
)
session.log("Running cocotb-config as basic installation smoke test")
session.run("cocotb-config", "--version")
session.log("Installing test dependencies")
session.run("pip", "install", *test_deps)
@nox.session
@nox.parametrize("sim,toplevel_lang,gpi_interface", simulator_support_matrix())
def release_test_sim(
session: nox.Session, sim: str, toplevel_lang: str, gpi_interface: str
) -> None:
"""Test a release version of cocotb against a simulator."""
release_install(session)
env = env_vars_for_test(sim, toplevel_lang, gpi_interface)
config_str = stringify_dict(env)
session.log(f"Running tests against a simulator: {config_str}")
session.run("make", "test", external=True, env=env)
session.log(f"Running simulator-specific tests against a simulator {config_str}")
session.run(
"pytest",
"-v",
"-k",
"simulator_required",
env=env,
)
session.log(f"All tests passed with configuration {config_str}!")
@nox.session
def release_test_nosim(session: nox.Session) -> None:
"""Run the simulator-agnostic tests against a cocotb release."""
release_install(session)
session.log("Running simulator-agnostic tests")
session.run(
"pytest",
"-v",
"-k",
"not simulator_required",
)
session.log("All tests passed!")
def create_env_for_docs_build(session: nox.Session) -> None:
session.run(
"pip", "install", "docs/_vendor/domaintools"
) # not done in requirements.txt due to the way relative paths are handled in that file (gh-pypa/pip#8765)
session.run("pip", "install", "-r", "docs/requirements.txt")
@nox.session
def docs(session: nox.Session) -> None:
"""invoke sphinx-build to build the HTML docs"""
create_env_for_docs_build(session)
session.run("pip", "install", ".")
outdir = session.cache_dir / "docs_out"
session.run("sphinx-build", "./docs/source", str(outdir), "--color", "-b", "html")
index = (outdir / "index.html").resolve().as_uri()
session.log(f"Documentation is available at {index}")
@nox.session
def docs_preview(session: nox.Session) -> None:
"""Build a live preview of the documentation"""
create_env_for_docs_build(session)
# Editable install allows editing cocotb source and seing it updated in the live preview
session.run("pip", "install", "-e", ".")
session.run("pip", "install", "sphinx-autobuild")
outdir = session.cache_dir / "docs_out"
session.run(
"sphinx-autobuild",
# Ignore directories which cause a rebuild loop.
"--ignore",
"*/source/master-notes.rst",
"--ignore",
"*/doxygen/*",
# Also watch the cocotb source directory to rebuild the API docs on
# changes to cocotb code.
"--watch",
"src/cocotb",
"./docs/source",
str(outdir),
)
@nox.session
def docs_linkcheck(session: nox.Session) -> None:
"""invoke sphinx-build to linkcheck the docs"""
create_env_for_docs_build(session)
session.run("pip", "install", ".")
outdir = session.cache_dir / "docs_out"
session.run(
"sphinx-build",
"./docs/source",
str(outdir),
"--color",
"-b",
"linkcheck",
)
@nox.session
def docs_spelling(session: nox.Session) -> None:
"""invoke sphinx-build to spellcheck the docs"""
create_env_for_docs_build(session)
session.run("pip", "install", ".")
outdir = session.cache_dir / "docs_out"
session.run(
"sphinx-build",
"./docs/source",
str(outdir),
"--color",
"-b",
"spelling",
)
@nox.session(reuse_venv=True)
def dev(session: nox.Session) -> None:
"""Build a development environment and optionally run a command given as extra args"""
configure_env_for_dev_build(session)
create_env_for_docs_build(session)
session.run(
"pip", "install", *test_deps, *dev_deps, *coverage_deps, *coverage_report_deps
)
session.run("pip", "install", "-e", ".")
if session.posargs:
session.run(*session.posargs, external=True)