1use 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
56const 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
64const DEV_INSPECT_FEATURES: [&str; 6] = [
67 "dev-inspect",
68 "rightkit-tauri-shell/dev-inspect",
69 "rightkit-tauri-shell?/dev-inspect",
70 "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
80fn 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
108fn 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
118fn 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
150fn 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
158fn 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
180fn 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
191fn 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
211fn 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 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 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 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 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 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 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 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}