Skip to content

Implement module extension for compilation tests [POC]#190

Draft
mateusz-olczyk wants to merge 3 commits intoEngFlow:mainfrom
mateusz-olczyk:module-extension-poc
Draft

Implement module extension for compilation tests [POC]#190
mateusz-olczyk wants to merge 3 commits intoEngFlow:mainfrom
mateusz-olczyk:module-extension-poc

Conversation

@mateusz-olczyk
Copy link
Collaborator

@mateusz-olczyk mateusz-olczyk commented Feb 2, 2026

A solution proposal for #189

This is a POC, lacking documentation, etc. I'll split it into logical parts if you accept this. Currently, only 2 test scenarios are included as examples:

  • subdirectory_basic
  • virtual_include_paths

How does it work?

Module generation

The module extension creates a new module for each test workspace. In the first phase, the contents of the test workspace are recursively copied into the new module. Actually, most of the files are symlinked for efficiency. BUILD.out files are renamed into BUILD.bazel files, so Bazel can interpret them and recognize packages. At this moment, we can already test the compilation with:

bazel build @generated_repo//...

Target aggregation

It would be great to include those targets in the command:

bazel build //...

This would create an effective feedback loop for the developer, allowing for an instant check for the success of a test scenario while working on a feature. But those targets belong to external, just-generated repositories. We need to aggregate them somehow.

Wildcards like ..., :all, :* are not allowed outside the CLI, i.e., in the Starlark code. We need to manually aggregate the targets within filegroups.

Filegroups generation

Now comes the tricky part. generate_filegroups.sh scans BUILD files using very primitive pattern matching and appends filegroups, so eventually we end up with labels like @generated_repo//:all_workspace_rules. With the help of gazelle_compilation_test macro, we include these into the root repository, so they become a part of //... wildcard and they're built together with gazelle_cc targets. So, actually gazelle_compilation_test does not define a Bazel test (it is just an alias for an external filegroup), but semantically we can call it a test.

I've decided to use a simple shell script because of limitation of the repository_rule. At this phase, Bazel targets do not even exist. So we cannot just use a go_binary as a tool. A shell script is simple; it depends only on basic commands like find or grep.

We could involve more correct parsing logic, similar to BUILD files discovery of gazelle. But it requires compiling a Go code manually, outside of Bazel rules logic. This is exactly how gazelle is built in bazel-gazelle (see go_repository_tools rule).

Summary

gazelle_compilation_tests module extension discovers the test workspaces under the given directory and includes them in the //... wildcard, giving the developer instant feedback about the compilation success.

No bazel_integration_test is used anywhere.

Signed-off-by: Mateusz Olczyk <molczyk@virtuslab.com>
Signed-off-by: Mateusz Olczyk <molczyk@virtuslab.com>
Signed-off-by: Mateusz Olczyk <molczyk@virtuslab.com>
doc = "Files with this suffix will be renamed to .bazel in the repository",
),
},
local = True,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is local absolutely needed? I think this causes the repo rule to be re-evaluated on every build, whether anything has changed or not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

According to repository_rule docs:

Indicate that this rule fetches everything from the local system and should be reevaluated at every fetch.

I had a problem with that, because the first statement: rule fetches everything from the local system is true, while I'm not sure about the second: should be reevaluated at every fetch. I'll experiment with turning this flag off and see if the repository_rule correctly reloads needed data when modifying test scenarios. Especially, I explicitly call watch, so it should do the job.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I haven't actually used one of these rules before, so I'm not really sure of the details. I vaguely thought they were for auto-configuring non-hermetic toolchains. For example, you could check the Xcode version and write that to a file that other targets depend on, so they would be rebuilt if you updated Xcode. (Not sure how accurate this impression is though)


def _compilation_test_repo_impl(repository_ctx):
source_dir = repository_ctx.attr.source_dir
filegroup_tool = repository_ctx.path(Label("@gazelle_cc//:scripts/generate_filegroups.sh"))
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be an attribute, rather than a dynamically generated label.

repository_ctx.symlink(source_file, dest_path)

# Generate filegroups
result = repository_ctx.execute([filegroup_tool, "-m", MAIN_FILEGROUP_NAME, "."] + repository_ctx.attr.rule_kinds)
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like we're running a pretty substantial script here.

In general, it is a good idea to put all commands in a script rather than trying to execute commands from Starlark. Can we move the commands above into the script so the logic isn't so fragmented?

Use caution in general though: Bash probably won't work on Windows, so we're going to need to copy and translate into Powershell, which is a pain. Keep this as minimal as possible.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In general, it is a good idea to put all commands in a script rather than trying to execute commands from Starlark.

Bash probably won't work on Windows

I'll try to do the opposite at first - maybe I'd be able to prepare a pure Starlark implementation. So no shells would be required at all.


return ctx.execute(find_command)

def _compilation_test_repo_impl(repository_ctx):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Need some explanation as a doc string here or on the rule about what this does and why. I see that it's listing files and doing something with a filegroup, but the outcome is not at all clear from this context.

@@ -0,0 +1,153 @@
#!/usr/bin/env sh
set -eu
Copy link
Collaborator

Choose a reason for hiding this comment

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

More explicit and check for pipefail:

set -o errexit -o nounset -o pipefail

Copy link
Collaborator

Choose a reason for hiding this comment

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

So I see that this is creating filegroup targets. It feels heavy to me though. I wonder if it's better to just write a filegroup manually for each test? I'm mainly concerned that:

  1. It's going to be a pain to duplicate and maintain this in Powershell for Windows.
  2. This modifies the golden output files (I think?) in a way that's not obvious to the test writer.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I understand your concerns. But all this magic with filegroups has a deep sense from my point of view. This module extension should provide kind of a "framework" which allows easy extending already existing scenarios with a compilation checking. So, in theory, for valid scenarios, you'd be expected just to transparently include them in the new framework without any additional changes needed, especially in the test data content. If some changes are required, they should indicate that the scenario contained some real compilation errors from the very beginning, not because it is not adapted to the new test framework.

In my opionion it's up to the test framework to automatically provide some "boilerplate" to make the test runnable. Limiting the responsibility of the module extension only to a simple copying of contents with ".out"-".bazel" renaming seems to be incomplete. It's like half-automation. Some work is done for you, but in the end, you're forced to do something with your test layout anyway.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's fair. I guess most of my concern is that it's written in Bash and wouldn't be compatible with Windows. If it's possible to do it in Starlark, that would be fine, though Starlark is a very limited language. A self-contained Python script might be a good alternative. We could reasonably expect a Python interpreter to be installed in CI.

Copy link
Collaborator Author

@mateusz-olczyk mateusz-olczyk left a comment

Choose a reason for hiding this comment

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

Thank you, @jayconrod, for all the comments. I'll see what I can do.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I understand your concerns. But all this magic with filegroups has a deep sense from my point of view. This module extension should provide kind of a "framework" which allows easy extending already existing scenarios with a compilation checking. So, in theory, for valid scenarios, you'd be expected just to transparently include them in the new framework without any additional changes needed, especially in the test data content. If some changes are required, they should indicate that the scenario contained some real compilation errors from the very beginning, not because it is not adapted to the new test framework.

In my opionion it's up to the test framework to automatically provide some "boilerplate" to make the test runnable. Limiting the responsibility of the module extension only to a simple copying of contents with ".out"-".bazel" renaming seems to be incomplete. It's like half-automation. Some work is done for you, but in the end, you're forced to do something with your test layout anyway.

repository_ctx.symlink(source_file, dest_path)

# Generate filegroups
result = repository_ctx.execute([filegroup_tool, "-m", MAIN_FILEGROUP_NAME, "."] + repository_ctx.attr.rule_kinds)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In general, it is a good idea to put all commands in a script rather than trying to execute commands from Starlark.

Bash probably won't work on Windows

I'll try to do the opposite at first - maybe I'd be able to prepare a pure Starlark implementation. So no shells would be required at all.

doc = "Files with this suffix will be renamed to .bazel in the repository",
),
},
local = True,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

According to repository_rule docs:

Indicate that this rule fetches everything from the local system and should be reevaluated at every fetch.

I had a problem with that, because the first statement: rule fetches everything from the local system is true, while I'm not sure about the second: should be reevaluated at every fetch. I'll experiment with turning this flag off and see if the repository_rule correctly reloads needed data when modifying test scenarios. Especially, I explicitly call watch, so it should do the job.

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