diff --git a/_posts/2026-09-01-cocotb-2.1.md b/_posts/2026-09-01-cocotb-2.1.md new file mode 100644 index 0000000..a93b6e5 --- /dev/null +++ b/_posts/2026-09-01-cocotb-2.1.md @@ -0,0 +1,168 @@ +--- +title: "cocotb 2.1.0 released" +date: 2026-09-01 +author: Kaleb Barrett +--- + +This release focuses on improving user quality-of-life, improving documentation, and setting cocotb up for even more ambitious feature work. +It is backwards compatible with the 2.0 release. + +Check out the list below for the changes we are most proud of and think are the most impactful. +If you want the full list, check out the [release notes](https://docs.cocotb.org/en/v2.1.0/release_notes.html). + +## New Concurrency Primitives + +### `gather` and `select` + +`cocotb.triggers.First` and `cocotb.triggers.Combine` support awaiting multiple Triggers at the same time; +`First` waiting for any to finish, and `Combine` waiting for all. +However, they have some serious limitations, the most annoying being that they don't support being passed Python native coroutines directly. + +To address the issues with these functions, new concurrency primitives were added. +`select` and `gather` were introduced to replace `First` and `Combine`, respectively. +These new functions support being passed coroutines, or any awaitable object directly, +and return more information to the user. + +For a full tutorial on these functions and the rest of cocotb's concurrency system, [check out the new docs!](https://docs.cocotb.org/en/v2.1.0/scheduler.html) + +```python +async def drive(intf, transaction_queue): + while True: + await RisingEdge(intf.clk) + intf.valid.value = 0 + + # idx is the index into the argument list + # res is the result of the argument that finished + idx, res = await select( + transaction_queue.get(), # coroutine!!! + RisingEdge(intf.rst), # Trigger + ) + + if idx == 0: + intf.data.value = res.data + intf.valid.value = 1 + else: + # got reset, hold valid low until reset is done + intf.valid.value = 0 + await FallingEdge(intf.rst) +``` + +### `TaskManager` + +[trio](https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning) introduced to the Python world structured concurrency with `Nursery`s. +The Python standard library [asyncio](https://docs.python.org/3/library/asyncio-task.html#asyncio.TaskGroup) followed with `TaskGroup`. +Even SystemVerilog has `fork`/`join`. +Now cocotb has `TaskManager` to fill the same role. +If `gather` or `select` are not expressive enough for your needs, `TaskManager` will likely work for you. + +```python +async with TaskManager() as tm: + + @tm.fork + async def drive_intf_a(intf): + while True: + ... # drive interface A + + @tm.fork + async def drive_intf_b(intf): + while True: + ... # drive interface B + +# When the TaskManager block exits it implicitly waits for all Tasks to finish before continuing. +print("Done!") +``` + +## cocotb Previews + +Som improvements to cocotb can only be made by breaking something. +Breaking changes are always considered very carefully, and when they occur cocotb gains a new major version. +We, the maintainers, do try to prevent major versions from happening too often to make life a bit easier on our users. +But the flip side of that coin is that multiple breaking changes can amass over time making it too painful for users to move to the new major version. + +To solve this issue, cocotb has gained a new system: "Previews." +Enabling Previews allows cocotb maintainers and developers to make breaking changes without having to wait several years before getting the opportunity. +And it allows users to opt-in to the new breaking behavior so they get feedback sooner about whether their codebase is ready for the next major version. + +Previews are enabled by setting the `COCOTB_PREVIEW` environment variable. +You can set it to a [comma separated list of features](https://docs.cocotb.org/en/v2.1.0/library_reference.html#envvar-COCOTB_PREVIEW), allowing you to adapt to each change, one at a time; +or you can simply set it to `1` to enable all changes. + +How you use this feature is up to you. +Will you leave it off, checking it occasionally, allowing your codebase to take advantage of the backwards compatability guarantee of the cocotb 2.X major release cycle? +Or will you leave it set to `1`, taking advantage of the newest features and changes on every minor release, thus spreading out the major version upgrade work? + +## Stackable Test Decorators + +The `@cocotb.test` decorator has a number of arguments for specifying conditions on when to skip tests, mark them as expected to fail or raise an exception, and more. +But this makes more complex uses difficult as all "skip" conditions have to be joined into a single ugly expression. + +To fix this, we added new "stackable" test decorators: `@cocotb.skipif` and `@cocotb.xfail`, +and also made `@cocotb.parametrize` stackable as well. +Each cocotb test can take multiple decorators and every condition they specify is joined together before tests are discovered. + +```python +@cocotb.skipif(cocotb.top.ENABLE_ETH_B.value == 0, reason="This tests ethernet B, so skip if the device is not configured with it.") +@cocotb.xfail(today() == "Monday", reason="Mondays are the worst.") +@cocotb.parametrize( + packet_size=[1, 10, 200, 2000] +) +@cocotb.parametrize( + traffic_generator=["realistic", "stress"] +) +async def test_switch(dut, packet_size: int, traffic_generator: str) -> None: + ... +``` + +## Test Forcing Functions + +If you've ever written a test where the decision the test has passed happens deep in some random Task, +or if you've been unable to express the logic to skip a test in the test decorator, +cocotb now has a set of test end forcing functions to help you. + +Users can now call `pytest.skip` and `pytest.xfail` in tests to immediately end the test with a forced SKIP or XFAIL result. + +```python +@cocotb.parametrize( + valid_fraction=[1.0, 0.6] + ready_fraction=[1.0, 0.6], +) +async def test_control_flow(dut, valid_fraction: float, ready_fraction: float) -> None: + + if dut.BUFFERED_OUTPUT.value and ready_fraction != 1.0: + pytest.skip("If the device is configured with the buffered output, we will never see ready go low.") + + ... +``` + +Similarly, the `cocotb.end_test` function will end the test immediately as if the main test function had ended. + +```python +@cocotb.test +async def test_watchdog(dut): + + async def watch_watchdog(): + await RisingEdge(dut.watchdog) + # End the test from this Task instead of the main test coroutine. + cocotb.end_test() + + # Run the watcher in the background. + cocotb.start_soon(watch_watchdog()) + + ... # Stimulate a hang. +``` + +## Scheduler Rewrite + +One of the major efforts of the release cycle should be invisible to the user. +The coroutine scheduler that cocotb uses to run Tasks concurrently and react to Triggers firing has been rewritten. +This rewrite was designed to improve performance, improve readbility and maintainability, and set cocotb up for scheduler-related enhancements in the coming versions. + +## Platform Support Changes + +Technology progresses and we eventually must leave behind old platforms that have become difficult to maintain. +32-bit builds for Linux and Windows are no longer available. +Support for Python 3.6, 3.7, and 3.8 have been removed. +MacOS 13 support has also been removed. + +On the other hand, cocotb gained support for Python 3.14, Ubuntu 26.04 and MacOS 26. +Leaving old Python versions behind allows cocotb to use new Python features to ensure correctness and maintainability. \ No newline at end of file