Skip to content

Expose user enabled features as pytest markers#508

Merged
arvind4501 merged 1 commit into
theforeman:masterfrom
ekohl:add-feature-test-infra
May 19, 2026
Merged

Expose user enabled features as pytest markers#508
arvind4501 merged 1 commit into
theforeman:masterfrom
ekohl:add-feature-test-infra

Conversation

@ekohl
Copy link
Copy Markdown
Member

@ekohl ekohl commented May 14, 2026

Why are you introducing these changes? (Problem description, related links)

I suggested it in #484 (comment) because I think it sets the base for future extensibility.

What are the changes introduced in this pull request?

It reads foremanctl features to know which features exist and which are enabled. It then configures pytest to expose a feature marker (in pytest_configure) that's then read in pytest_runtest_setup.

With all of that set up, the existing iop feature guard is rewritten.

How to test this pull request

Steps to reproduce:

  • Let CI run (or locally)
  • Verify that it skips IOP when the feature is disabled
  • Verify that it runs IOP tests when the feature is enabled

Checklist

  • Tests added/updated (if applicable)
  • Documentation updated (if applicable)

@ekohl
Copy link
Copy Markdown
Member Author

ekohl commented May 14, 2026

Is this a more common failure?

:stderr: ERROR: Could not install packages due to an OSError: [Errno 28] No space left on device: '/home/runner/smoker/venv/lib/python3.12/site-packages/certifi-2026.4.22.dist-info'

Comment thread tests/conftest.py Outdated
@property
def _obsah_state(self) -> Path:
# mirror what foremanctl / obsah does
if state := os.environ.get('OBSAH_STATE'):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am wondering if OBSAH_STATE and OBSAH_BASE will exist until we define them, in case of foremanctl we define them to be used in execution in https://github.com/theforeman/foremanctl/blob/master/foremanctl

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I had assumed that you could still override them, but now that I read foremanctl again I see I was wrong.

My idea was to build towards running things in parallel:

export OBSAH_STATE=$(mktemp -d)
foremanctl deploy --flavor foreman
pytest

Then in another shell:

export OBSAH_STATE=$(mktemp -d)
foremanctl deploy --flavor katello
pytest

Perhaps it's better to reduce this method to the hardcoded option until we get the other pieces in place.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So these envs are no use as of today, but when we have support for multiple flavours(which certanily will have diffrent set of features ) we could use envs at runtime to invoke flavour specific ci workflows?

Thinking out loud:- In foreman-installer we had answers file per scenarios, will we have similar kind of parameters files for each flavour, also there was no way of changing scenarios after you deployed with one, will that remain same or could be improved,
This is not related to the PR, but just wanted to write my mind dump out

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

So these envs are no use as of today, but when we have support for multiple flavours(which certanily will have diffrent set of features ) we could use envs at runtime to invoke flavour specific ci workflows?

Yes. I was a bit inspired by #494. That uses multiple git checkouts (which has a value in its own) while the approach I had in mind here allowed testing multiple deployments from the same directory. But thinking more about it, perhaps we shouldn't build that partially and either do it completely or not at all.

Thinking out loud:- In foreman-installer we had answers file per scenarios, will we have similar kind of parameters files for each flavour, also there was no way of changing scenarios after you deployed with one, will that remain same or could be improved,

Technically there was a way to switch scenarios, but that switching was never really well implemented / supported. Partially because the underlying system didn't really support it. I think on the end user system we should aim for a single parameters file and not allow switching between flavors.

This is not related to the PR, but just wanted to write my mind dump out

Since we're dumping thoughts: I've been toying with the thought of remote deployments. Ansible supports running on remote hosts so can we imagine some sort of control node where you manage the actual deployment? Then by changing OBSAH_STATE you could perhaps manage your various environments (like test, production, etc).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we imagine some sort of control node where you manage the actual deployment?

Yes, that should be possible with ansible, rather than hardcoding quadlet as deployment location we could give a target group(multiple hosts).

Comment thread tests/conftest.py Outdated
return root / '.var' / 'lib' / 'foremanctl'

def _read_parameters(self):
params_file = self._obsah_state / 'parameters.yaml'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not blocking: currently we are reading from parameters.yaml, and today if we add feature it first gets added to parameters.yaml and then the subsequent steps for setup runs in playbook and roles.
The only case i see this becoming a issue is when --add-feature fails in between the run but feature persists in parameters.yaml and then that leaves that feature specific test broken.

I also a relevent PR which persits params after playbook run theforeman/obsah#118

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'd argue that's a corrupted state and running a test suite on that isn't useful. If foremanctl deploy exits with a non-zero code then IMHO the whole execution should halt.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

After playing with the tests myself I now understand what you mean. The test suite runs --add-feature invalid-feature which invalidates reruns.

I've also found another limitation: it doesn't read the features from the flavor which for now isn't a big deal but will be when you really want to gate all features.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Should it use foremanctl features --list-enabled to get the list instead of digging in internals?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should it use foremanctl features --list-enabled to get the list instead of digging in internals?

Yes that would be a better option as foremanctl features will retrieve features from features.yml.

I've also found another limitation: it doesn't read the features from the flavor which for now isn't a big deal but will be when you really want to gate all features.

This will also be solved if we use foremanctl features to get features list. one thing to note here is that foremanctl features is not aware about flavor while pulling features, so it pulls all features from feeatures.yml, which is fine as of today because we only have one flavor.

But in future we need to make foremanctl features aware of flavor

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

But that also brings next challenge which is what if mutiple features needs dynflow as dependency.

You read my mind. I was thinking about Apache & PostgreSQL and if that could be tracked as an internal feature. It can be pulled in by multiple services (Foreman, Candlepin, Pulp).

Copy link
Copy Markdown
Contributor

@arvind4501 arvind4501 May 18, 2026

Choose a reason for hiding this comment

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

I was thinking about Apache & PostgreSQL and if that could be tracked as an internal feature.

not sure if we would expose postgresql as feature, but we have remote-execution which has dynflow as dependency but we might also have dynflow as dependency for other feature, i think ansible and that way we have two features having dynflow as dependency,

and then with what i said earlier

i am thinking is test user exposed features and conditionalily test internal features based on if the feature (that needs them as dependency, so in case of dynflow, it is remote-execution), is enabled.

becomes a issue as we need to test dynflow if any of these feature is enabled.

I have also asked somewhat similar #444 (comment)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

not sure if we would expose postgresql as feature, but we have remote-execution which has dynflow as dependency but we might also have dynflow as dependency for other feature, i think ansible and that way we have two features having dynflow as dependency,

The reason I got to it is introducing a plain foreman-proxy (without content). That doesn't run Apache and PostgreSQL. I could derive it in Ansible code from which other features are enabled but then it's harder in testing. An internal feature could help.

I have also asked somewhat similar #444 (comment)

Good point, I hadn't thought that far ahead yet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yes, looking at

assert "dynflow" in features

we will mark it as pytest.mark.feature("foreman-proxy") probabily and i think we can stick to use
foremanctl feartures --list-enabled to get features (which will also bring default flavor feature) for now, and once we see more test required for internal features, we can extend that. what do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, let's start with foremanctl feartures --list-enabled and refactor as needed. IMHO even without the internal features this is already a useful framework.

Comment thread tests/conftest.py Outdated
Comment thread tests/conftest.py Outdated
Comment thread docs/developer/testing.md Outdated
Comment thread tests/conftest.py Outdated
@arvind4501
Copy link
Copy Markdown
Contributor

@coderabbitai

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Tip

For best results, initiate chat on the files or code changes.

@arvind4501 — To answer @ekohl's question: yes, this is a well-known and recurring issue with GitHub-hosted runners. The OSError: [Errno 28] No space left on device error during pip dependency installation is a common failure on GitHub Actions runners. It has been reported across many projects, and it tends to appear silently after periods of no issues, typically targeting Linux runners.

This failure is unrelated to the PR's code changes — it's a transient infrastructure issue. A simple re-run of the CI job usually resolves it. If it becomes persistent, some common mitigations include:

  1. Freeing disk space early in the workflow (e.g., removing unused tools like rm -rf /opt/hostedtoolcache or using an action like jlumbroso/free-disk-space):
    - name: Free disk space
      run: |
        sudo rm -rf /opt/hostedtoolcache
        docker system prune -af
  2. Re-running the failed job — transient space exhaustion on a shared runner often doesn't repeat.
  3. Using self-hosted runners with more storage, which gives you full control over hardware and storage capacity.

@arvind4501
Copy link
Copy Markdown
Contributor

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

✅ Actions performed

Full review triggered.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Review Change Stack

Walkthrough

This PR refactors the test feature marking system from IOP-specific hardcoding to a configuration-driven approach. A new UserParameters class reads feature availability from parameters.yaml in the OBSAH state directory. Pytest hooks register a parameterized feature(name) marker and enforce runtime test skipping when required features are missing. The test_foreman_proxy_features test is updated to dynamically check for the BMC feature via pytestconfig.user_parameters.features. All 14 IOP test modules are migrated from the pytest.mark.iop marker to pytest.mark.feature("iop"). Developer documentation is added explaining the feature guarding pattern.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: exposing user-enabled features as pytest markers to allow feature-based test selection.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description clearly describes the problem (setting a base for future extensibility), the changes (reading foremanctl features and configuring pytest markers), and testing instructions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
docs/developer/testing.md (1)

153-153: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix spelling error.

"invidivual" should be "individual".

📝 Proposed fix
-You can mark an invidivual test to be skipped if needed:
+You can mark an individual test to be skipped if needed:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/developer/testing.md` at line 153, Typo in the docs: replace the
misspelled word "invidivual" with the correct word "individual" in the sentence
"You can mark an invidivual test to be skipped if needed:" so the line reads
"You can mark an individual test to be skipped if needed:"; search for the exact
token "invidivual" in docs/developer/testing.md and update it wherever it
appears to maintain consistency.
tests/conftest.py (1)

37-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add error handling for YAML parsing and validation.

The current implementation has two error paths that would crash test collection:

  1. yaml.safe_load() can raise yaml.YAMLError if the file is malformed
  2. Line 49 assumes params is a dict, but yaml.safe_load() can return non-dict types (list, string, None, etc.), causing AttributeError
🛡️ Proposed fix to add robust error handling
 def _read_parameters(self):
     params_file = self._obsah_state / 'parameters.yaml'
     if params_file.exists():
-        with params_file.open('r') as f:
-            return yaml.safe_load(f)
+        try:
+            with params_file.open('r') as f:
+                params = yaml.safe_load(f)
+                if not isinstance(params, dict):
+                    return None
+                return params
+        except yaml.YAMLError:
+            return None
 
     return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/conftest.py` around lines 37 - 50, The _read_parameters/features pair
must guard against malformed YAML and unexpected types: wrap
yaml.safe_load(params_file) in a try/except catching yaml.YAMLError (and other
exceptions from reading) and return None on failure; in features, verify that
params is an instance of dict before accessing params.get and ensure the
'features' value is an iterable/list before converting to a set (otherwise treat
as empty), and optionally emit a warning/log when parsing or validation fails;
update _read_parameters, features, params_file, and yaml.safe_load usage
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@docs/developer/testing.md`:
- Line 153: Typo in the docs: replace the misspelled word "invidivual" with the
correct word "individual" in the sentence "You can mark an invidivual test to be
skipped if needed:" so the line reads "You can mark an individual test to be
skipped if needed:"; search for the exact token "invidivual" in
docs/developer/testing.md and update it wherever it appears to maintain
consistency.

In `@tests/conftest.py`:
- Around line 37-50: The _read_parameters/features pair must guard against
malformed YAML and unexpected types: wrap yaml.safe_load(params_file) in a
try/except catching yaml.YAMLError (and other exceptions from reading) and
return None on failure; in features, verify that params is an instance of dict
before accessing params.get and ensure the 'features' value is an iterable/list
before converting to a set (otherwise treat as empty), and optionally emit a
warning/log when parsing or validation fails; update _read_parameters, features,
params_file, and yaml.safe_load usage accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f6dea4a-b883-433c-aa2c-c0769cfd1414

📥 Commits

Reviewing files that changed from the base of the PR and between e531086 and 54a75eb.

📒 Files selected for processing (19)
  • docs/developer/testing.md
  • tests/conftest.py
  • tests/foreman_proxy_test.py
  • tests/iop/test_advisor.py
  • tests/iop/test_advisor_frontend.py
  • tests/iop/test_cvemap_downloader.py
  • tests/iop/test_engine.py
  • tests/iop/test_gateway.py
  • tests/iop/test_ingress.py
  • tests/iop/test_integration.py
  • tests/iop/test_inventory.py
  • tests/iop/test_inventory_frontend.py
  • tests/iop/test_kafka.py
  • tests/iop/test_puptoo.py
  • tests/iop/test_remediation.py
  • tests/iop/test_vmaas.py
  • tests/iop/test_vulnerability.py
  • tests/iop/test_vulnerability_frontend.py
  • tests/iop/test_yuptoo.py

@arvind4501
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@arvind4501
Copy link
Copy Markdown
Contributor

arvind4501 commented May 19, 2026

2 suggestions from coderabbit

  • Fix spelling error.

    "invidivual" should be "individual".

Thats indeed a good finding and actionable

Add error handling for YAML parsing and validation.

https://github.com/theforeman/foremanctl/pull/508/changes#diff-e52e4ddd58b7ef887ab03c04116e676f6280b824ab7469d5d3080e5cba4f2128R39-R41

I don't think we need to do error handling here, parameters.yaml is obsah's space, with foremanctl we just use what obsah gives and if there are possibilities of errors we handle in obsah rather than here

Sad part is it did not even considered foremanctl features --list-enabled over current logic in UserParameters class

@shubhamsg199
Copy link
Copy Markdown
Contributor

I don't think we need to do error handling here, parameters.yaml is obsah's space, with foremanctl we just use what obsah gives and if there are possibilities of errors we handle in obsah rather than here

This is something I also had in mind #508 (comment) but the scenario which i pointed out might not ever happen.

@ekohl ekohl force-pushed the add-feature-test-infra branch from 54a75eb to 21cc56f Compare May 19, 2026 09:25
@ekohl
Copy link
Copy Markdown
Member Author

ekohl commented May 19, 2026

I've updated it to call foremanctl features and parse the output. I chose not to pass --enabled-features and list them all so I could determine available_features. That allows me to raise a config warning when an invalid feature mark is used. I'm not sure if there is a better way to do this, but I verified it works by using @pytest.mark.feature('invalid').

@ekohl ekohl force-pushed the add-feature-test-infra branch 2 times, most recently from c66320c to eb6a3d9 Compare May 19, 2026 09:31
This allows guarding tests behind markers.
@ekohl ekohl force-pushed the add-feature-test-infra branch from eb6a3d9 to a1e80be Compare May 19, 2026 09:32
@ekohl
Copy link
Copy Markdown
Member Author

ekohl commented May 19, 2026

And now with a few force pushes I think I pleased the linter too.

@arvind4501 arvind4501 merged commit b6d8f08 into theforeman:master May 19, 2026
13 checks passed
@ekohl ekohl deleted the add-feature-test-infra branch May 19, 2026 10:47
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.

3 participants