Skip to content

Add pants support for pytest#323

Merged
nprizal merged 1 commit intobuildkite:mainfrom
altana-ai:feat/add-pants-support
Jun 11, 2025
Merged

Add pants support for pytest#323
nprizal merged 1 commit intobuildkite:mainfrom
altana-ai:feat/add-pants-support

Conversation

@jasonwbarnett
Copy link
Contributor

@jasonwbarnett jasonwbarnett commented May 30, 2025

Description

Adds experimental support for running pytest tests using pants via a new pytest-pants test runner in bktec. This enables teams using Pants to integrate with Buildkite Test Analytics while retaining core retry and mute capabilities.

Context

#315

Changes

  • Adds pytest-pants as a new supported runner type
  • Installs Pants CLI in the Dockerfile
  • Adds dedicated runner logic (pytest_pants.go) with validation and command construction
  • Routes pytest-pants test plans to the API as pytest for compatibility
  • Updates README feature matrix and docs
  • Adds a complete example with pants resolve+lockfile, requirements, and test cases

Testing

  • Added unit tests for pytest-pants in pytest_pants_test.go
  • Verified end-to-end with sample Pants project in testdata/pytest_pants
  • Confirmed retry and result collection work as expected
  • Validated in a private monorepo

@jasonwbarnett jasonwbarnett requested a review from a team as a code owner May 30, 2025 22:33
@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch 3 times, most recently from ea44b1b to bfeef2e Compare May 30, 2025 22:40
@jasonwbarnett
Copy link
Contributor Author

jasonwbarnett commented May 30, 2025

It works!

$ export BUILDKITE_TEST_ENGINE_TEST_RUNNER=pytest
$ export BUILDKITE_TEST_ENGINE_TEST_RUNNER_VARIANT=pants
$ export BUILDKITE_TEST_ENGINE_TEST_CMD="pants test --output=all src/python/devops/azure_devops/checks_test.py -- --json={{resultPath}}"
$ ~/git/test-engine-client/test-engine-client
+++ Buildkite Test Engine Client: bktec dev


______ ______ _____
___  /____  /___  /____________
__  __ \_  //_/  __/  _ \  ___/
_  /_/ /  ,<  / /_ /  __/ /__
/_.___//_/|_| \__/ \___/\___/

Info: Python package 'buildkite-test-collector' is required and will not be verified by bktec. Please ensure it is added to the pants resolve used by pytest.
Billing Error: Test Splitting is not enabled on your plan: please upgrade at https://buildkite.com/organizations/<redacted>/billing.
⚠️ Falling back to non-intelligent splitting. Your build may take longer than usual.
+++ Buildkite Test Engine Client: Running tests
pants test src/python/devops/azure_devops/checks_test.py -- --json=/var/folders/kw/qhf2k3js3nvgf7t8_1g46skm0000gq/T/bktec-pytest-1365635794/pytest-results.json

16:40:22.93 [INFO] Completed: Run Pytest - (environment:macos_arm64, src/python/devops/azure_devops/checks_test.py:tests) - succeeded.

✓ src/python/devops/azure_devops/checks_test.py:tests succeeded in 1.03s (ran in local environment `macos_arm64`).
+++ ========== Buildkite Test Engine Report  ==========
✅ All tests passed.

+---------+-----------+---+
| Passed  | first run | 6 |
+         +-----------+---+
|         | on retry  | 0 |
+---------+-----------+---+
| Muted   | passed    | 0 |
+         +-----------+---+
|         | failed    | 0 |
+---------+-----------+---+
| Failed  |           | 0 |
+---------+-----------+---+
| Skipped |           | 0 |
+---------+-----------+---+
|               TOTAL | 6 |
+---------+-----------+---+
===================================================

@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch 6 times, most recently from 693db65 to 2e09de0 Compare May 30, 2025 23:03
@jasonwbarnett
Copy link
Contributor Author

Since opening this PR, I’ve realized there’s a key issue that needs to be accounted for:

  1. test-engine-client assumes a single invocation of pytest, and therefore only a single {{resultPath}}.
    This doesn’t work with Pants (unless using xdist), because Pants spawns a separate pytest process per test—or per group of tests when using [batch_compatibility_tag][batch-compatibility-tag]. Each of these processes writes to the same JSON file, which results in only one set of test results being preserved. The others are overwritten.

One potential solution would be to update [test-collector-python] to use a [filelock][filelock] and append or merge results into the JSON file in a thread/process-safe way. This would allow multiple processes to contribute to a single result file without clobbering each other, and still keep things compatible with test-engine-client.

@nprizal
Copy link
Contributor

nprizal commented Jun 4, 2025

Hi @jasonwbarnett. Thank you for opening the PR. I might be missing some contexts here, but can you explain more about the support you want to add, or the use case you might have?

bktec is currently designed around test splitting, where multiple instances of the client run a subset of test files that it receives from the server as a Test Plan. However, looking at your changes, it seems like bktec won't do the splitting. Instead, pants will spawn multiple pytest processes, which is not what bktec is designed currently.

We do have other features, such as muting or retrying tests. If that's something you'd want to achieve, we could add the support, but but it might require a slightly different approach compared to other test runners with splitting support.

@jasonwbarnett
Copy link
Contributor Author

@nprizal Exactly! We’re primarily interested in the muting and retry features. Test splitting isn’t feasible in our case due to Pants, which already has richer context about the test structure. It’s not that it couldn’t be done, it’s just not a priority for us right now. What we’re really eager for is the ability to automatically mute flaky tests and allow developers to manually mark tests as flaky so their results can be excluded.

@nprizal
Copy link
Contributor

nprizal commented Jun 6, 2025

@jasonwbarnett Got it! After looking at your changes, it seems like the runner behaves slightly differently when using pants. The main thing that is the same is the results handling. Personally, I prefer to create a separate runner and reuse some of the shared functionality. This will make it easier if we want to implement a splitting feature for pants.

// pytest.go
type Pytest struct {}

func NewPytest(c Config) Pytest {
  ...
}

func (p Pytest) GetFiles() (string[], error) {
  ...
}

func (p Pytest) Run(...) error {
  ...
  err = runAndForwardSignal(cmd)
  ...
  err = HandlePytestResult(result, p.ResultPath)
  ...
}

func HandlePytestResult(result RunResult, path string) error { // this is the shared function
   tests, err := ParsePytestCollectorResult(p.ResultPath)
   ...
   for _, test := range tests {
     result.RecordTestResult(...)
   }
   ...
} 
// pytest_pants.go
type PytestPants struct {}

func NewPytestPants(c Config) {
  ...
}

func (p PytestPants) GetFiles() (string[], error) {
  return []string{}, nil
}

func (p PytestPants) Run(...) error {
  ...
  err = runAndForwardSignal(cmd)
  ...
  err = HandlePytestResult(result, p.ResultPath)
  ...
}

We are also planning of extracting the results handling, including the parsing, separately from the runner in the future. So that the runner package is responsible for running the tests, while the result_handler is responsible for parsing and recording the results.

internal
  /runner
    /pytest.go
    /pytest_pants.go
  /result_handler
    /pytest.go
    /run_result.go
    /test_result.go

Since opening this PR, I’ve realized there’s a key issue that needs to be accounted for:

  1. test-engine-client assumes a single invocation of pytest, and therefore only a single {{resultPath}}.
    This doesn’t work with Pants (unless using xdist), because Pants spawns a separate pytest process per test—or per group of tests when using [batch_compatibility_tag][batch-compatibility-tag]. Each of these processes writes to the same JSON file, which results in only one set of test results being preserved. The others are overwritten.

One potential solution would be to update [test-collector-python] to use a [filelock][filelock] and append or merge results into the JSON file in a thread/process-safe way. This would allow multiple processes to contribute to a single result file without clobbering each other, and still keep things compatible with test-engine-client.

This sounds like a sensible approach. My main concern is that in the current bktec pytest runner implementation, we create a temporary directory to store the test result if the BUILDKITE_TEST_ENGINE_RESULT_PATH is not set. This directory will be destroyed when bktec terminated. We can make the BUILDKITE_TEST_ENGINE_RESULT_PATH mandatory for pants so we can use that file for many pytest processes.

@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch 2 times, most recently from 48a75d7 to df291c5 Compare June 6, 2025 16:20
@jasonwbarnett
Copy link
Contributor Author

jasonwbarnett commented Jun 6, 2025

@nprizal I’ve updated the PR and implemented a distinct PytestPants runner, separate from the standard Pytest runner. It reuses the existing pytest results parsing logic.

This setup works, but note that the results are currently overwritten with each test run orchestrated by pants, so only one test result is retained. This will be resolved once buildkite/test-collector-python#55 is merged and published to PyPI.

I'm pretty sure CI should pass with the addition of pants into the docker image, but I'm not fully sure. Let me know if it needs any changes to pass.

@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch 2 times, most recently from 0e203d1 to e4eab4e Compare June 6, 2025 16:26
@jasonwbarnett jasonwbarnett changed the title add pants support to pytest runner feat: add pants support for pytest Jun 6, 2025
@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch 5 times, most recently from 0cdde1b to 54b2420 Compare June 6, 2025 16:49
@jasonwbarnett
Copy link
Contributor Author

Tests are passing locally:

Running tool: /opt/homebrew/bin/go test -timeout 30s -run ^(TestPytestPantsRun|TestPytestPantsRun_RetryCommand|TestPytestPantsRun_TestFailed|TestPytestPantsRun_CommandFailed|TestPytestPantsGetFiles|TestPytestPantsGetExamples|TestPytestPantsCommandNameAndArgs_WithResultPath|TestPytestPantsCommandNameAndArgs_WithoutResultPath|TestPytestPantsCommandNameAndArgs_InvalidTestCommand)$ github.com/buildkite/test-engine-client/internal/runner

ok  	github.com/buildkite/test-engine-client/internal/runner	2.420s

@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch 2 times, most recently from 98e5300 to 35dc597 Compare June 6, 2025 17:24
@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch 2 times, most recently from 539a2b6 to 0ff4bd8 Compare June 6, 2025 17:28
@jasonwbarnett
Copy link
Contributor Author

I just finished adding documentation to this PR. I'm a little confused what to label and name things because pants can technically run many different types of tests, i.e. python, javascript, kotlin, golang, scala, typescript, etc

This PR is strictly aiming to support Python which is arguably the most predominantly used language in pants.

I'm open to changing the naming of things as you see fit.

Cheers!

@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch from 0ff4bd8 to 7365401 Compare June 6, 2025 17:37
@jasonwbarnett
Copy link
Contributor Author

I confirmed the --merge-json flag works as expected.


with --merge-json

$ export BUILDKITE_TEST_ENGINE_TEST_CMD="pants --filter-target-type=python_test test //src/python/devops/azure_devops:: -- --json={{resultPath}} --merge-json"
$ ~/git/test-engine-client/test-engine-client
+++ Buildkite Test Engine Client: bktec dev


______ ______ _____
___  /____  /___  /____________
__  __ \_  //_/  __/  _ \  ___/
_  /_/ /  ,<  / /_ /  __/ /__
/_.___//_/|_| \__/ \___/\___/

Info: Python package 'buildkite-test-collector' is required and will not be verified by bktec. Please ensure it is added to the pants resolve used by pytest.
2025/06/06 14:08:02 DEBUG: Fetching test plan
2025/06/06 14:08:02 DEBUG: Sending request GET https://api.buildkite.com/v2/analytics/organizations/org/suites/test-out-new-thing/test_plan?identifier=<redacted>/<redacted>&job_retry_count=0
2025/06/06 14:08:03 DEBUG: Response code 404
2025/06/06 14:08:03 DEBUG: No test plan found, creating a new plan
2025/06/06 14:08:03 DEBUG: Test plan created. Identifier: "<redacted>/<redacted>"
2025/06/06 14:08:03 DEBUG: My favourite ice cream is
+++ Buildkite Test Engine Client: Running tests
pants --filter-target-type=python_test test //src/python/devops/azure_devops:: -- --json=/var/folders/kw/qhf2k3js3nvgf7t8_1g46skm0000gq/T/bktec-pytest-189074465/pytest-results.json --merge-json

Skipping automatic ecr-login because PANTS_SKIP_AUTO_ECR_LOGIN is set to true
14:08:16.89 [INFO] Completed: Run Pytest - (environment:macos_arm64, src/python/devops/azure_devops/checks_test.py:tests) - succeeded.
14:08:16.90 [INFO] Completed: Run Pytest - (environment:macos_arm64, src/python/devops/azure_devops/pull_request_description_test.py:tests) - succeeded.
14:08:16.99 [INFO] Completed: Run Pytest - (environment:macos_arm64, src/python/devops/azure_devops/releases/notes/helpers_test.py:tests) - succeeded.

✓ src/python/devops/azure_devops/checks_test.py:tests succeeded in 1.47s (ran in local environment `macos_arm64`).
✓ src/python/devops/azure_devops/pull_request_description_test.py:tests succeeded in 1.47s (ran in local environment `macos_arm64`).
✓ src/python/devops/azure_devops/releases/notes/helpers_test.py:tests succeeded in 1.56s (ran in local environment `macos_arm64`).
+++ ========== Buildkite Test Engine Report  ==========
✅ All tests passed.

+---------+-----------+----+
| Passed  | first run | 18 |
+         +-----------+----+
|         | on retry  |  0 |
+---------+-----------+----+
| Muted   | passed    |  0 |
+         +-----------+----+
|         | failed    |  0 |
+---------+-----------+----+
| Failed  |           |  0 |
+---------+-----------+----+
| Skipped |           |  0 |
+---------+-----------+----+
|               TOTAL | 18 |
+---------+-----------+----+
===================================================

Without --merge-json

$ export BUILDKITE_TEST_ENGINE_TEST_CMD="pants --filter-target-type=python_test test //src/python/devops/azure_devops:: -- --json={{resultPath}}"
$ ~/git/test-engine-client/test-engine-client
+++ Buildkite Test Engine Client: bktec dev


______ ______ _____
___  /____  /___  /____________
__  __ \_  //_/  __/  _ \  ___/
_  /_/ /  ,<  / /_ /  __/ /__
/_.___//_/|_| \__/ \___/\___/

Info: Python package 'buildkite-test-collector' is required and will not be verified by bktec. Please ensure it is added to the pants resolve used by pytest.
2025/06/06 14:08:02 DEBUG: Fetching test plan
2025/06/06 14:08:02 DEBUG: Sending request GET https://api.buildkite.com/v2/analytics/organizations/org/suites/test-out-new-thing/test_plan?identifier=<redacted>/<redacted>&job_retry_count=0
2025/06/06 14:08:03 DEBUG: Response code 404
2025/06/06 14:08:03 DEBUG: No test plan found, creating a new plan
2025/06/06 14:08:03 DEBUG: Test plan created. Identifier: "<redacted>/<redacted>"
2025/06/06 14:08:03 DEBUG: My favourite ice cream is
+++ Buildkite Test Engine Client: Running tests
pants --filter-target-type=python_test test //src/python/devops/azure_devops:: -- --json=/var/folders/kw/qhf2k3js3nvgf7t8_1g46skm0000gq/T/bktec-pytest-189074465/pytest-results.json --merge-json

Skipping automatic ecr-login because PANTS_SKIP_AUTO_ECR_LOGIN is set to true
14:08:16.89 [INFO] Completed: Run Pytest - (environment:macos_arm64, src/python/devops/azure_devops/checks_test.py:tests) - succeeded.
14:08:16.90 [INFO] Completed: Run Pytest - (environment:macos_arm64, src/python/devops/azure_devops/pull_request_description_test.py:tests) - succeeded.
14:08:16.99 [INFO] Completed: Run Pytest - (environment:macos_arm64, src/python/devops/azure_devops/releases/notes/helpers_test.py:tests) - succeeded.

✓ src/python/devops/azure_devops/checks_test.py:tests succeeded in 1.47s (ran in local environment `macos_arm64`).
✓ src/python/devops/azure_devops/pull_request_description_test.py:tests succeeded in 1.47s (ran in local environment `macos_arm64`).
✓ src/python/devops/azure_devops/releases/notes/helpers_test.py:tests succeeded in 1.56s (ran in local environment `macos_arm64`).
+++ ========== Buildkite Test Engine Report  ==========
✅ All tests passed.

+---------+-----------+----+
| Passed  | first run | 6 |
+         +-----------+----+
|         | on retry  |  0 |
+---------+-----------+----+
| Muted   | passed    |  0 |
+         +-----------+----+
|         | failed    |  0 |
+---------+-----------+----+
| Failed  |           |  0 |
+---------+-----------+----+
| Skipped |           |  0 |
+---------+-----------+----+
|               TOTAL | 6 |
+---------+-----------+----+
===================================================

@jasonwbarnett
Copy link
Contributor Author

I'm blocked until buildkite/test-collector-python#55 is merged and published on pypi.org. The tests and code depend on it now that I'm planning to require --merge-json argument.

Copy link
Contributor

@nprizal nprizal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good. I have comments around the naming and the test plan creation. I have also turned on the 3rd party contributor build, so next time you push a commit, it should trigger a CI build in the pipeline.

main.go Outdated
debug.Println("Creating test plan")
testPlan, err := apiClient.CreateTestPlan(ctx, cfg.SuiteSlug, params)
var testPlan plan.TestPlan
if params.Runner != "pytestpants" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to create the test plan to get the list of muted tests from the server.

Copy link
Contributor Author

@jasonwbarnett jasonwbarnett Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good catch — I meant to pull that out before opening the PR. I started looking into where the list of supported runners is coming from, and it seemed like it was hardcoded in the server-side API, since the request fails when using pytestpants and returns a list of valid runners. But now I’m second-guessing myself — it might actually be this client that’s doing the filtering, and I just failed to track it down properly.

When main.go is reverted to the main branch I get this:

Buildkite Test Engine Client: Couldn't fetch or create test plan: Invalid parameters: Runner must be one of "rspec", "jest", "cypress", "playwright", "gotest", "pytest", "cucumber"

It does appear to be that the server side API is validating the client and since this is new, it's throwing it out. Maybe I need to figure out how to send pytest to the call, even though it's coming from the pytest-pants runner?

Mind taking a quick look or pointing me in the right direction?

Copy link
Contributor Author

@jasonwbarnett jasonwbarnett Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found where it's building params for the request and just swapped it with pytest (source here). Let me know if I should do something else here.

@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch from ff33821 to d8d858c Compare June 9, 2025 20:43
@jasonwbarnett jasonwbarnett requested a review from nprizal June 9, 2025 20:53
@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch 2 times, most recently from cdfa8c3 to db467c9 Compare June 10, 2025 01:38
@jasonwbarnett
Copy link
Contributor Author

@nprizal Can you unblock the build pipeline?

@jasonwbarnett
Copy link
Contributor Author

@nprizal Thanks for the support. It looks like the tests are passing! 🎉 🎉

@jasonwbarnett
Copy link
Contributor Author

@nprizal I tested everything end to end in our monorepo. Things look good.

Copy link
Contributor

@nprizal nprizal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great 🚀! The changes are isolated, and won't affect the existing pytest runner and other common behavior. I have few non blocking comments.

}, nil
}

if cfg.TestRunner == "pytest-pants" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can maybe add a comment to explain why this is necessary?

@jasonwbarnett
Copy link
Contributor Author

This looks great 🚀! The changes are isolated, and won't affect the existing pytest runner and other common behavior. I have few non blocking comments.

Thanks! I'll get to this in an hour.

@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch from c042e79 to bf5ade5 Compare June 11, 2025 01:28
@jasonwbarnett
Copy link
Contributor Author

jasonwbarnett commented Jun 11, 2025

@nprizal I've updated the code with the two requested changes. Should I squash all commits or will you do that on merge?

Copy link
Contributor

@nprizal nprizal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasonwbarnett Thanks for adding the comment! Could you please squash the commits? Once you’ve done that, I’ll merge the PR and create a release candidate so your team can begin testing it.

@jasonwbarnett jasonwbarnett force-pushed the feat/add-pants-support branch from bf5ade5 to b785e85 Compare June 11, 2025 01:46
@jasonwbarnett
Copy link
Contributor Author

@nprizal squashed and pushed.

@nprizal nprizal changed the title feat: add pants support for pytest Add pants support for pytest Jun 11, 2025
@nprizal nprizal merged commit 9271e18 into buildkite:main Jun 11, 2025
1 check passed
@jasonwbarnett jasonwbarnett deleted the feat/add-pants-support branch June 11, 2025 02:40
@nprizal
Copy link
Contributor

nprizal commented Jun 11, 2025

@jasonwbarnett I have created a release candidate that includes this PR
https://github.com/buildkite/test-engine-client/releases/tag/v1.6.0-rc.1

@jasonwbarnett
Copy link
Contributor Author

I just realized I failed to update the PR description before this was merged. It's wildly inaccurate. Is there anyway to fix it?

@nprizal
Copy link
Contributor

nprizal commented Jun 11, 2025

@jasonwbarnett I think you can still edit the PR description.

@jasonwbarnett
Copy link
Contributor Author

@nprizal Great. The PR description is updated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants