diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1197e827..9c777fc6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,3 +42,15 @@ jobs: - name: test (only enable x86 and arm64) run: env ${{ matrix.rust.env }} FEATURES=std,full,arch_x86,arch_arm64 NO_DEFAULT_FEATURES=1 ./capstone-rs/ci/test.sh + + feature-checks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + - name: test feature combinations + run: python3 scripts/feature-checks.py diff --git a/capstone-rs/src/arch/mod.rs b/capstone-rs/src/arch/mod.rs index 7ab81e78..7d93db6a 100644 --- a/capstone-rs/src/arch/mod.rs +++ b/capstone-rs/src/arch/mod.rs @@ -676,15 +676,19 @@ macro_rules! detail_defs { /// architecture's detail structure. This allows you to use an `if let Some(...) = { /* ... */ }` /// instead of a match statement. #[derive(Debug)] + #[non_exhaustive] pub enum ArchDetail<'a> { $( #[cfg(feature = $feature)] $Detail($InsnDetail), )+ + #[doc(hidden)] + _Lifetime(PhantomData<&'a ()>), } /// Architecture-independent enum of operands #[derive(Clone, Debug, PartialEq)] + #[non_exhaustive] pub enum ArchOperand { $( #[cfg(feature = $feature)] @@ -705,6 +709,7 @@ macro_rules! detail_defs { vec } )+ + ArchDetail::_Lifetime(_) => unreachable!(), } } diff --git a/scripts/feature-checks.py b/scripts/feature-checks.py new file mode 100755 index 00000000..4b66330a --- /dev/null +++ b/scripts/feature-checks.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Test capstone-rs with various feature combinations. + +Tests: + - all features disabled + - all features enabled + - each arch feature individually enabled + - random subsets (2-5 arch features) + - std/full on/off combinations +""" + +import random +import re +import subprocess +import sys +from pathlib import Path +from typing import List, Optional + +REPO_ROOT = Path(__file__).resolve().parent.parent +MANIFEST = REPO_ROOT / "capstone-rs" / "Cargo.toml" +CARGO = "cargo" + + +def cargo_check( + label: str, + *, + default_features: bool = True, + all_features: bool = False, + features: Optional[List[str]] = None, +) -> Optional[str]: + args = ["--manifest-path", str(MANIFEST)] + if all_features: + args.append("--all-features") + else: + if not default_features: + args.append("--no-default-features") + if features: + args.extend(["--features", ",".join(features)]) + + cmd = [CARGO, "check"] + args + print(f"\n=== {label} ===") + print(f" {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode == 0: + print(" PASS") + return None + print(result.stdout) + print(result.stderr) + print(" FAIL") + return label + + +def get_arch_features() -> List[str]: + """Extract arch feature names from Cargo.toml.""" + text = MANIFEST.read_text() + features = [] + for line in text.splitlines(): + m = re.match(r"^(arch_\w+) = \[\"capstone-sys/arch_", line) + if m: + features.append(m.group(1)) + return sorted(features) + + +def test_std_full_combos() -> List[str]: + """Test various combinations of std and full being on/off.""" + failed: List[str] = [] + for std in (False, True): + for full in (False, True): + feats = [] + if std: + feats.append("std") + if full: + feats.append("full") + label = f"std={std},full={full}" + result = cargo_check( + label, + default_features=False, + features=feats or None, + ) + if result: + failed.append(result) + return failed + + +def main() -> None: + arch_features = get_arch_features() + print(f"Found {len(arch_features)} arch features:") + for f in arch_features: + print(f" {f}") + + failed: List[str] = [] + + # 1. All features disabled + r = cargo_check("no-default-features", default_features=False) + if r: + failed.append(r) + + # 2. All features enabled + r = cargo_check("all-features", all_features=True) + if r: + failed.append(r) + + # 3. std/full combinations + failed.extend(test_std_full_combos()) + + # 4. Each arch feature individually + for feat in arch_features: + r = cargo_check( + f"only-{feat}", + default_features=False, + features=["std", "full", feat], + ) + if r: + failed.append(r) + + # 5. Random subsets: shuffle each trial independently + rng = random.Random(42) + for trial in range(1, 6): + shuffled = sorted(arch_features) + rng.shuffle(shuffled) + n = rng.choice([2, 3, 4, 5]) + subset = shuffled[:n] + label = f"subset-trial{trial}-{n}arch" + r = cargo_check( + label, + default_features=False, + features=["std", "full", *subset], + ) + if r: + failed.append(r) + + print() + print("=" * 50) + if not failed: + print("All feature combinations passed!") + else: + print(f"FAILED ({len(failed)}):") + for f in failed: + print(f" {f}") + sys.exit(1) + + +if __name__ == "__main__": + main()