Skip to main content

rightkit_qa/
guard.rs

1//! The guard that keeps the in-app QA control surface out of builds that must not carry it.
2//!
3//! Two backends are understood: `control` (the in-app `rightkit-qa` `control` server (or the legacy `rightkit-qa-driver` shim),
4//! the default) and `wdio` (the legacy `tauri-plugin-wdio-webdriver`, kept so an
5//! app mid-migration still validates).
6//!
7//! The rule: the server is compiled only behind an explicit `qa-native` cargo feature.
8//! `qa-native` is off by default and only dev lanes enable it. A dev build may contain the
9//! server (it may use a release-derived profile, so `debug_assertions` is not required).
10//! A release or update build must not contain it; `right-release` refuses to seal one that does.
11use crate::util::{err, Result};
12use serde_json::Value;
13use std::collections::{BTreeMap, BTreeSet};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Backend {
17    Control,
18    Wdio,
19}
20
21impl Backend {
22    pub fn parse(s: &str) -> Option<Self> {
23        match s {
24            "control" => Some(Backend::Control),
25            "wdio" => Some(Backend::Wdio),
26            _ => None,
27        }
28    }
29    fn dependency(self) -> &'static str {
30        match self {
31            Backend::Control => "rightkit-qa-driver",
32            Backend::Wdio => "tauri-plugin-wdio-webdriver",
33        }
34    }
35    fn registration(self) -> &'static str {
36        match self {
37            Backend::Control => "rightkit_qa_driver::embedded::Control",
38            Backend::Wdio => "tauri_plugin_wdio_webdriver::init",
39        }
40    }
41    fn runtime_flag(self) -> &'static str {
42        match self {
43            Backend::Control => "RIGHTKIT_CONTROL_SERVICE",
44            Backend::Wdio => "RIGHTKIT_QA_NATIVE",
45        }
46    }
47}
48
49pub struct ContractInput<'a> {
50    pub cargo_toml: &'a str,
51    pub rust_source: &'a str,
52    pub package_scripts: &'a BTreeMap<String, String>,
53    pub backend: Backend,
54}
55
56/// Compiled-in gates that accept a registration: the feature alone (the current rule), or
57/// the older debug-only form. Matched against whitespace-free source text.
58const QA_NATIVE_GATES: [&str; 3] = [
59    r#"#[cfg(feature="qa-native")]"#,
60    r#"#[cfg(all(debug_assertions,feature="qa-native"))]"#,
61    r#"#[cfg(all(feature="qa-native",debug_assertions))]"#,
62];
63
64/// Feature items that switch on rightkit-tauri-shell developer inspection. None may be reachable from
65/// `default`: a release build must never contain it (`packages/release` refuses it as well).
66const DEV_INSPECT_FEATURES: [&str; 6] = [
67    "dev-inspect",
68    "rightkit-tauri-shell/dev-inspect",
69    "rightkit-tauri-shell?/dev-inspect",
70    // Apps still on the old published name (rightkit-shell) stay covered until they migrate.
71    "rightkit-shell/dev-inspect",
72    "rightkit-shell?/dev-inspect",
73    "dev-native",
74];
75
76fn compact(text: &str) -> String {
77    text.chars().filter(|c| !c.is_whitespace()).collect()
78}
79
80/// Text of `name = { ... }` (inline table) or the `[dependencies.name]` table.
81fn dependency_body(cargo: &str, name: &str) -> Option<String> {
82    let mut search = 0;
83    while let Some(i) = cargo[search..].find(name) {
84        let at = search + i;
85        search = at + name.len();
86        let prev = if at == 0 {
87            b' '
88        } else {
89            cargo.as_bytes()[at - 1]
90        };
91        let before_ok = !prev.is_ascii_alphanumeric() && prev != b'-' && prev != b'_';
92        let rest = cargo[at + name.len()..].trim_start();
93        if before_ok && rest.starts_with('=') {
94            let rest = rest[1..].trim_start();
95            if let Some(body) = rest.strip_prefix('{') {
96                return body.find('}').map(|e| body[..e].to_string());
97            }
98            return Some(String::new());
99        }
100    }
101    cargo.find(&format!("[dependencies.{name}]")).map(|i| {
102        let tail = &cargo[i..];
103        let end = tail[1..].find("\n[").map(|e| e + 1).unwrap_or(tail.len());
104        tail[..end].to_string()
105    })
106}
107
108/// Text of the `[features]` table, up to the next table header.
109fn features_table(cargo: &str) -> &str {
110    let Some(start) = cargo.find("[features]") else {
111        return "";
112    };
113    let body = &cargo[start + "[features]".len()..];
114    let end = body.find("\n[").unwrap_or(body.len());
115    &body[..end]
116}
117
118/// Every `name = [ ... ]` entry of the `[features]` table, as (name, list body). A list may
119/// continue over several lines.
120fn feature_entries(cargo: &str) -> Vec<(String, String)> {
121    let table = features_table(cargo);
122    let mut out = Vec::new();
123    let mut pos = 0;
124    while pos < table.len() {
125        let line_end = table[pos..].find('\n').map_or(table.len(), |i| pos + i + 1);
126        let trimmed = table[pos..line_end].trim();
127        if let Some((key, value)) = trimmed.split_once('=') {
128            let key = key.trim();
129            if let Some(list) = value.trim_start().strip_prefix('[') {
130                if !key.is_empty() && !key.starts_with('#') {
131                    let mut body = list.to_string();
132                    let mut end = line_end;
133                    while !body.contains(']') && end < table.len() {
134                        let next = table[end..].find('\n').map_or(table.len(), |i| end + i + 1);
135                        body.push_str(&table[end..next]);
136                        end = next;
137                    }
138                    let close = body.find(']').unwrap_or(body.len());
139                    out.push((key.to_string(), body[..close].to_string()));
140                    pos = end;
141                    continue;
142                }
143            }
144        }
145        pos = line_end;
146    }
147    out
148}
149
150/// Items of a feature list body such as `"dep:rightkit-qa-driver", "rightkit-qa-driver/qa-control"`.
151fn list_items(list: &str) -> Vec<String> {
152    list.split(',')
153        .map(|s| s.trim().trim_matches('"').trim().to_string())
154        .filter(|s| !s.is_empty())
155        .collect()
156}
157
158/// Whether `default`, directly or through other features, enables `target`.
159fn default_reaches(entries: &[(String, String)], target: &str) -> bool {
160    let mut stack: Vec<String> = entries
161        .iter()
162        .filter(|(k, _)| k.as_str() == "default")
163        .flat_map(|(_, v)| list_items(v))
164        .collect();
165    let mut seen = BTreeSet::new();
166    while let Some(item) = stack.pop() {
167        if item == target {
168            return true;
169        }
170        if !seen.insert(item.clone()) {
171            continue;
172        }
173        if let Some((_, list)) = entries.iter().find(|(k, _)| k.as_str() == item.as_str()) {
174            stack.extend(list_items(list));
175        }
176    }
177    false
178}
179
180/// False for a `use` line or a comment line. Those name the registration path but do not
181/// register anything, so they need no gate.
182fn is_code_at(src: &str, at: usize) -> bool {
183    let line_start = src[..at].rfind('\n').map_or(0, |p| p + 1);
184    let line = src[line_start..].lines().next().unwrap_or("").trim_start();
185    !(line.starts_with("use ")
186        || line.starts_with("pub use ")
187        || line.starts_with("pub(crate) use ")
188        || line.starts_with("//"))
189}
190
191/// Whether the code at `at` sits under an accepted qa-native gate. The gate may sit on the
192/// statement itself, or on an enclosing function or block, so the walk moves outward
193/// through `{` boundaries and stops at the first `;` or `}`.
194fn is_gated(src: &str, at: usize) -> bool {
195    let mut end = at;
196    for _ in 0..8 {
197        let found = src[..end].rfind([';', '{', '}']);
198        let head_start = found.map_or(0, |p| p + 1);
199        let head = compact(&src[head_start..end]);
200        if QA_NATIVE_GATES.iter().any(|gate| head.contains(*gate)) {
201            return true;
202        }
203        match found {
204            Some(p) if src.as_bytes()[p] == b'{' => end = p,
205            _ => return false,
206        }
207    }
208    false
209}
210
211/// The control-server dependency an app uses: `(dependency, feature, registration path)`.
212/// An app on `rightkit-qa` (feature `control`) is validated against that shape; the legacy
213/// `rightkit-qa-driver` (feature `qa-control`) shape stays accepted for apps not yet migrated.
214fn control_target(cargo: &str) -> (&'static str, &'static str, &'static str) {
215    if dependency_body(cargo, "rightkit-qa-driver").is_none()
216        && dependency_body(cargo, "rightkit-qa").is_some()
217    {
218        ("rightkit-qa", "control", "rightkit_qa::embedded::Control")
219    } else {
220        (
221            "rightkit-qa-driver",
222            "qa-control",
223            "rightkit_qa_driver::embedded::Control",
224        )
225    }
226}
227
228pub fn validate(input: &ContractInput) -> Vec<String> {
229    let mut issues = vec![];
230    let control = input.backend == Backend::Control;
231    let (dep, qa_feat, registration) = if control {
232        control_target(input.cargo_toml)
233    } else {
234        (
235            input.backend.dependency(),
236            "qa-control",
237            input.backend.registration(),
238        )
239    };
240    let dep_body = dependency_body(input.cargo_toml, dep).map(|b| compact(&b));
241    let dep_enables_qa_control = dep_body
242        .as_deref()
243        .is_some_and(|b| b.contains(&format!("\"{qa_feat}\"")));
244    match &dep_body {
245        None => issues.push(format!(
246            "Cargo.toml must declare {dep} under [dependencies] as an optional dependency: \
247             `{dep} = {{ version = \"...\", optional = true, default-features = false }}`"
248        )),
249        Some(body) => {
250            if !body.contains("optional=true") {
251                issues.push(format!(
252                    "{dep} must be optional: declare it with `optional = true` so that only \
253                     the qa-native feature compiles the QA server into the app"
254                ));
255                if control && dep_enables_qa_control {
256                    issues.push(format!(
257                        "{dep} enables the qa-control feature without optional = true, so the \
258                         QA server compiles into every build: make the dependency optional and \
259                         enable qa-control only from qa-native"
260                    ));
261                }
262            }
263        }
264    }
265
266    let entries = feature_entries(input.cargo_toml);
267    let dep_feature = format!("dep:{dep}");
268    let qa_control_feature = format!("{dep}/{qa_feat}");
269    match entries.iter().find(|(k, _)| k.as_str() == "qa-native") {
270        None => issues.push(format!(
271            "Cargo.toml must define the qa-native feature under [features], for example \
272             `qa-native = [\"dep:{dep}\", \"{dep}/{qa_feat}\"]`"
273        )),
274        Some((_, list)) => {
275            let items = list_items(list);
276            if !items.iter().any(|i| i.as_str() == dep_feature.as_str()) {
277                issues.push(format!("qa-native must enable {dep_feature}"));
278            }
279            let enables_control_qa = items
280                .iter()
281                .any(|i| i.as_str() == qa_control_feature.as_str());
282            if control && !enables_control_qa && !dep_enables_qa_control {
283                issues.push(format!(
284                    "qa-native must enable {qa_control_feature} (add it to the qa-native \
285                     list, or list {qa_feat} in the {dep} dependency's features); \
286                     without it the embedded server is not compiled"
287                ));
288            }
289        }
290    }
291    if default_reaches(&entries, "qa-native") {
292        issues.push(
293            "qa-native must not be enabled by default: remove it from `default` (directly or \
294             through another default feature) so that only `--features qa-native` builds \
295             contain the QA server"
296                .into(),
297        );
298    }
299    for target in DEV_INSPECT_FEATURES {
300        if default_reaches(&entries, target) {
301            issues.push(format!(
302                "{target} must not be enabled by default: it compiles developer inspection \
303                 (rightkit-tauri-shell dev-inspect) into the app, and right-release refuses to seal \
304                 or upload such a build. Remove it from `default` (directly or through another \
305                 default feature) so that only an explicit dev build contains it"
306            ));
307        }
308    }
309    // The crate was renamed from rightkit-shell; apps still on the old published name stay covered.
310    for shell_crate in ["rightkit-tauri-shell", "rightkit-shell"] {
311        if let Some(body) = dependency_body(input.cargo_toml, shell_crate).map(|b| compact(&b)) {
312            if body.contains("\"dev-inspect\"") && !body.contains("optional=true") {
313                issues.push(format!(
314                    "{shell_crate} lists the dev-inspect feature without optional = true, so \
315                     developer inspection compiles into every build: enable it only from an \
316                     optional dev feature such as `dev-native = [\"{shell_crate}/dev-inspect\"]`"
317                ));
318            }
319        }
320    }
321    for (name, list) in &entries {
322        if name.as_str() == "qa-native" {
323            continue;
324        }
325        let items = list_items(list);
326        if items.iter().any(|i| {
327            i.as_str() == dep_feature.as_str() || i.as_str() == qa_control_feature.as_str()
328        }) {
329            issues.push(format!(
330                "feature {name} enables {dep}; only qa-native may enable the QA server \
331                 dependency, so remove it from {name}"
332            ));
333        }
334    }
335
336    let sites: Vec<usize> = input
337        .rust_source
338        .match_indices(registration)
339        .map(|(at, _)| at)
340        .filter(|&at| is_code_at(input.rust_source, at))
341        .collect();
342    if sites.is_empty() {
343        issues.push(format!(
344            "the app must register {dep} in its Rust source: `{registration}` inside \
345             `#[cfg(feature = \"qa-native\")]`"
346        ));
347    }
348    let ungated: Vec<usize> = sites
349        .iter()
350        .copied()
351        .filter(|&at| !is_gated(input.rust_source, at))
352        .collect();
353    if let Some(&first) = ungated.first() {
354        let line = input.rust_source[..first].matches('\n').count() + 1;
355        issues.push(format!(
356            "{dep} registration `{registration}` is unconditional ({} place(s), first at line \
357             {line} of the Rust sources): gate each registration statement, or the function or \
358             block that contains it, with `#[cfg(feature = \"qa-native\")]` so that a build \
359             without the qa-native feature does not contain the QA server",
360            ungated.len()
361        ));
362    }
363
364    let rust_compact = compact(input.rust_source);
365    let runtime_gated = rust_compact.contains(input.backend.runtime_flag())
366        || (control && rust_compact.contains("build_if_enabled"));
367    if !runtime_gated {
368        issues.push(format!(
369            "registration must also require the {} runtime flag",
370            input.backend.runtime_flag()
371        ));
372    }
373
374    for (name, command) in input.package_scripts {
375        if command.contains("dev-inspect") || command.contains("dev-native") {
376            let lower = name.to_lowercase();
377            let dev_lane = ["qa", "test", "dev"]
378                .iter()
379                .any(|p| name == p || name.starts_with(&format!("{p}:")));
380            if !dev_lane
381                && ["release", "publish", "dmg", "installer", "package"]
382                    .iter()
383                    .any(|w| lower.contains(w))
384            {
385                issues.push(format!(
386                    "package script {name} must not enable dev-inspect or dev-native; only \
387                     qa, test or dev scripts may"
388                ));
389            }
390        }
391        if !command.contains("qa-native") {
392            continue;
393        }
394        if ["qa", "test", "dev"]
395            .iter()
396            .any(|p| name == p || name.starts_with(&format!("{p}:")))
397        {
398            continue;
399        }
400        let lower = name.to_lowercase();
401        if ["release", "publish", "build", "dmg", "installer", "package"]
402            .iter()
403            .any(|w| lower.contains(w))
404            || command.contains("tauri build")
405        {
406            issues.push(format!(
407                "package script {name} must not enable qa-native; only qa, test or dev scripts may"
408            ));
409        }
410    }
411    issues
412}
413
414pub fn assert_contract(input: &ContractInput) -> Result<()> {
415    let issues = validate(input);
416    if issues.is_empty() {
417        Ok(())
418    } else {
419        err(format!(
420            "native QA contract failed:\n- {}",
421            issues.join("\n- ")
422        ))
423    }
424}
425
426pub fn scripts_from_package_json(text: &str) -> Result<BTreeMap<String, String>> {
427    let v: Value = serde_json::from_str(text)?;
428    Ok(v.get("scripts")
429        .and_then(Value::as_object)
430        .map(|m| {
431            m.iter()
432                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
433                .collect()
434        })
435        .unwrap_or_default())
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    /// Shape in the README: the server is pulled in only by `qa-native`.
443    const CARGO: &str = r##"
444[features]
445default = []
446qa-native = ["dep:rightkit-qa-driver", "rightkit-qa-driver/qa-control"]
447[dependencies]
448rightkit-qa-driver = { version = "0.3", optional = true, default-features = false }
449"##;
450
451    /// The alternative shape: qa-control listed in the optional dependency's own features.
452    const CARGO_DEP_FEATURES: &str = r##"
453[features]
454default = []
455qa-native = ["dep:rightkit-qa-driver"]
456[dependencies]
457rightkit-qa-driver = { version = "0.3", optional = true, default-features = false, features = ["qa-control"] }
458"##;
459
460    /// Registration gated by the feature alone; the registration also appears in a `use` line.
461    const RUST_GATED: &str = r##"
462use rightkit_qa_driver::embedded::Control;
463fn run() {
464    #[cfg(feature = "qa-native")]
465    if let Some(p) = rightkit_qa_driver::embedded::Control::<tauri::Wry>::new().build_if_enabled() { b = b.plugin(p); }
466}"##;
467
468    /// The older debug-only form, with its compile_error. Still accepted.
469    const RUST_DEBUG_ONLY: &str = r##"
470#[cfg(all(not(debug_assertions), feature = "qa-native"))]
471compile_error!("qa-native is debug only");
472fn run() {
473    #[cfg(all(debug_assertions, feature = "qa-native"))]
474    if let Some(p) = rightkit_qa_driver::embedded::Control::<tauri::Wry>::new().build_if_enabled() { b = b.plugin(p); }
475}"##;
476
477    /// The gate sits on the function that holds the registration.
478    const RUST_FN_GATED: &str = r##"
479#[cfg(feature = "qa-native")]
480fn register(b: Builder) -> Builder {
481    if let Some(p) = rightkit_qa_driver::embedded::Control::<tauri::Wry>::new().build_if_enabled() { b.plugin(p) } else { b }
482}"##;
483
484    const RUST_UNGATED: &str = r##"
485use rightkit_qa_driver::embedded::Control;
486fn run() {
487    if let Some(p) = rightkit_qa_driver::embedded::Control::<tauri::Wry>::new().build_if_enabled() { b = b.plugin(p); }
488}"##;
489
490    fn check(cargo: &str, rust: &str, scripts: &[(&str, &str)], backend: Backend) -> Vec<String> {
491        let scripts: BTreeMap<String, String> = scripts
492            .iter()
493            .map(|(k, v)| (k.to_string(), v.to_string()))
494            .collect();
495        validate(&ContractInput {
496            cargo_toml: cargo,
497            rust_source: rust,
498            package_scripts: &scripts,
499            backend,
500        })
501    }
502
503    fn control(cargo: &str, rust: &str, scripts: &[(&str, &str)]) -> Vec<String> {
504        check(cargo, rust, scripts, Backend::Control)
505    }
506
507    fn any_contains(issues: &[String], needle: &str) -> bool {
508        issues.iter().any(|s| s.contains(needle))
509    }
510
511    #[test]
512    fn accepts_qa_native_gate_without_debug_assertions() {
513        let issues = control(
514            CARGO,
515            RUST_GATED,
516            &[("qa", "cargo run --features qa-native")],
517        );
518        assert_eq!(issues, Vec::<String>::new());
519    }
520
521    #[test]
522    fn accepts_older_debug_only_form() {
523        let issues = control(
524            CARGO,
525            RUST_DEBUG_ONLY,
526            &[("qa", "cargo run --features qa-native")],
527        );
528        assert_eq!(issues, Vec::<String>::new());
529    }
530
531    #[test]
532    fn accepts_gate_on_enclosing_function() {
533        assert_eq!(control(CARGO, RUST_FN_GATED, &[]), Vec::<String>::new());
534    }
535
536    #[test]
537    fn accepts_qa_control_listed_in_dependency_features() {
538        assert_eq!(
539            control(CARGO_DEP_FEATURES, RUST_GATED, &[]),
540            Vec::<String>::new()
541        );
542    }
543
544    #[test]
545    fn accepts_wdio_backend_gated_by_qa_native() {
546        let cargo = r##"
547[features]
548default = []
549qa-native = ["dep:tauri-plugin-wdio-webdriver"]
550[dependencies]
551tauri-plugin-wdio-webdriver = { version = "0.1", optional = true }
552"##;
553        let rust = r##"
554const FLAG: &str = "RIGHTKIT_QA_NATIVE";
555#[cfg(feature = "qa-native")]
556fn run() { b = b.plugin(tauri_plugin_wdio_webdriver::init()); }
557"##;
558        let issues = check(cargo, rust, &[], Backend::Wdio);
559        assert_eq!(issues, Vec::<String>::new());
560    }
561
562    /// The optional dev feature that selects rightkit-tauri-shell developer inspection.
563    const CARGO_DEV_INSPECT: &str = r##"
564[features]
565default = []
566qa-native = ["dep:rightkit-qa-driver", "rightkit-qa-driver/qa-control"]
567dev-native = ["rightkit-tauri-shell/dev-inspect"]
568[dependencies]
569rightkit-qa-driver = { version = "0.3", optional = true, default-features = false }
570rightkit-tauri-shell = { version = "0.4", default-features = false }
571"##;
572
573    #[test]
574    fn accepts_dev_inspect_behind_an_off_by_default_dev_feature() {
575        assert_eq!(
576            control(
577                CARGO_DEV_INSPECT,
578                RUST_GATED,
579                &[("dev", "cargo run --features dev-native")]
580            ),
581            Vec::<String>::new()
582        );
583    }
584
585    #[test]
586    fn rejects_dev_inspect_in_default_features() {
587        for default in [
588            "default = [\"dev-native\"]",
589            "default = [\"rightkit-tauri-shell/dev-inspect\"]",
590            "default = [\"dev-inspect\"]",
591            "default = [\"shell\"]\nshell = [\"dev-native\"]",
592        ] {
593            let cargo = CARGO_DEV_INSPECT.replace("default = []", default);
594            let issues = control(&cargo, RUST_GATED, &[]);
595            assert!(
596                any_contains(&issues, "must not be enabled by default"),
597                "{default}: {issues:?}"
598            );
599        }
600    }
601
602    #[test]
603    fn rejects_non_optional_dev_inspect_dependency_and_release_scripts() {
604        let cargo = CARGO_DEV_INSPECT.replace(
605            "rightkit-tauri-shell = { version = \"0.4\", default-features = false }",
606            "rightkit-tauri-shell = { version = \"0.4\", features = [\"dev-inspect\"] }",
607        );
608        let issues = control(&cargo, RUST_GATED, &[]);
609        assert!(
610            any_contains(&issues, "without optional = true"),
611            "{issues:?}"
612        );
613        let issues = control(
614            CARGO_DEV_INSPECT,
615            RUST_GATED,
616            &[("build:release", "tauri build --features dev-native")],
617        );
618        assert!(
619            any_contains(
620                &issues,
621                "package script build:release must not enable dev-inspect"
622            ),
623            "{issues:?}"
624        );
625    }
626
627    #[test]
628    fn accepts_and_checks_the_rightkit_qa_control_shape() {
629        let cargo = CARGO
630            .replace("rightkit-qa-driver", "rightkit-qa")
631            .replace("qa-control", "control");
632        let rust = RUST_GATED.replace("rightkit_qa_driver", "rightkit_qa");
633        assert_eq!(control(&cargo, &rust, &[]), Vec::<String>::new());
634        let without = cargo.replace(", \"rightkit-qa/control\"", "");
635        assert!(any_contains(
636            &control(&without, &rust, &[]),
637            "rightkit-qa/control"
638        ));
639        let eager = cargo.replace("optional = true, ", "");
640        assert!(any_contains(
641            &control(&eager, &rust, &[]),
642            "must be optional"
643        ));
644    }
645
646    #[test]
647    fn rejects_unconditional_registration() {
648        let issues = control(CARGO, RUST_UNGATED, &[]);
649        assert!(any_contains(&issues, "is unconditional"), "{issues:?}");
650    }
651
652    #[test]
653    fn rejects_negated_gate() {
654        let negated = RUST_GATED.replace(
655            "#[cfg(feature = \"qa-native\")]",
656            "#[cfg(not(feature = \"qa-native\"))]",
657        );
658        let issues = control(CARGO, &negated, &[]);
659        assert!(any_contains(&issues, "is unconditional"), "{issues:?}");
660    }
661
662    #[test]
663    fn rejects_release_scripts_and_non_optional_dependency() {
664        let cargo = CARGO.replace("optional = true, ", "");
665        let issues = control(
666            &cargo,
667            RUST_GATED,
668            &[("release", "tauri build --features qa-native")],
669        );
670        assert!(any_contains(&issues, "must be optional"), "{issues:?}");
671        assert!(
672            any_contains(&issues, "package script release"),
673            "{issues:?}"
674        );
675    }
676
677    #[test]
678    fn rejects_non_optional_dependency_that_enables_qa_control() {
679        let cargo = CARGO_DEP_FEATURES.replace("optional = true, ", "");
680        let issues = control(&cargo, RUST_GATED, &[]);
681        assert!(any_contains(&issues, "must be optional"), "{issues:?}");
682        assert!(
683            any_contains(
684                &issues,
685                "enables the qa-control feature without optional = true"
686            ),
687            "{issues:?}"
688        );
689    }
690
691    #[test]
692    fn rejects_qa_native_in_default_features() {
693        let cargo = CARGO.replace("default = []", "default = [\"qa-native\"]");
694        let issues = control(&cargo, RUST_GATED, &[]);
695        assert!(
696            any_contains(&issues, "must not be enabled by default"),
697            "{issues:?}"
698        );
699    }
700
701    #[test]
702    fn rejects_qa_native_reached_through_a_default_feature() {
703        let cargo = CARGO.replace(
704            "default = []",
705            "default = [\"full\"]\nfull = [\"qa-native\"]",
706        );
707        let issues = control(&cargo, RUST_GATED, &[]);
708        assert!(
709            any_contains(&issues, "must not be enabled by default"),
710            "{issues:?}"
711        );
712    }
713
714    #[test]
715    fn rejects_qa_native_without_qa_control() {
716        let cargo = CARGO.replace(", \"rightkit-qa-driver/qa-control\"", "");
717        let issues = control(&cargo, RUST_GATED, &[]);
718        assert!(
719            any_contains(&issues, "rightkit-qa-driver/qa-control"),
720            "{issues:?}"
721        );
722    }
723
724    #[test]
725    fn rejects_other_feature_that_enables_the_dependency() {
726        let cargo = CARGO.replace(
727            "[dependencies]",
728            "browser-ui = [\"dep:rightkit-qa-driver\"]\n[dependencies]",
729        );
730        let issues = control(&cargo, RUST_GATED, &[]);
731        assert!(
732            any_contains(&issues, "feature browser-ui enables"),
733            "{issues:?}"
734        );
735    }
736
737    #[test]
738    fn rejects_missing_qa_native_feature() {
739        let cargo = CARGO.replace(
740            "qa-native = [\"dep:rightkit-qa-driver\", \"rightkit-qa-driver/qa-control\"]\n",
741            "",
742        );
743        let issues = control(&cargo, RUST_GATED, &[]);
744        assert!(
745            any_contains(&issues, "must define the qa-native feature"),
746            "{issues:?}"
747        );
748    }
749
750    #[test]
751    fn rejects_missing_registration() {
752        let issues = control(CARGO, "fn main(){}", &[]);
753        assert!(any_contains(&issues, "must register"), "{issues:?}");
754    }
755}