majordomus_cli/quality/model.rs
1//! The vocabulary of a quality report: what was measured, what is wrong, and what a
2//! reader — a person, a pipeline or an agent — is to do about it.
3//!
4//! # Why the finding is typed
5//!
6//! A gate that prints `quality failed` is hostile infrastructure. Every finding here
7//! carries a stable [`ViolationCode`], the [rule](ViolationCode::rule) that requires it,
8//! the file and line it happened at, the symbol it is about, and a remediation in the
9//! imperative. That is what lets one measurement render as a terminal report, a JSON
10//! document, an HTTP response, a Cockpit panel and a CI annotation without any of them
11//! deciding anything of their own — the same relationship every other projection in this
12//! executable has with the registry.
13//!
14//! # The shape
15//!
16//! ```text
17//! QualityReport
18//! ├── public_api : items measured, documented, exampled
19//! ├── modules : modules measured, documented, exampled, behaviourally tested
20//! ├── operations : canonical capabilities against CLI, HTTP, OpenAPI, MCP
21//! └── violations : every finding, most severe first
22//! ```
23//!
24//! ```
25//! use majordomus_cli::quality::{QualityReport, Severity};
26//!
27//! // An empty report is a clean report, and a clean report passes.
28//! let report = QualityReport::default();
29//! assert!(report.passes());
30//! assert_eq!(report.exit_code(), 0);
31//! assert_eq!(report.count(Severity::Error), 0);
32//! ```
33
34use serde::{Deserialize, Serialize};
35
36/// The schema of a quality report. Bumped when a consumer would have to change.
37pub const SCHEMA: &str = "majordomus/quality/v1";
38
39/// How much a finding matters.
40///
41/// There are two levels and not five, because the only question a gate can answer is
42/// whether the change may land. A warning is a finding the repository has decided not to
43/// block on; nothing else is a warning.
44#[derive(
45 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
46)]
47#[serde(rename_all = "snake_case")]
48#[schemars(rename = "QualitySeverity")]
49pub enum Severity {
50 /// The gate fails.
51 Error,
52 /// Reported, and the gate passes.
53 Warning,
54}
55
56impl Severity {
57 /// The word a rendering shows.
58 ///
59 /// ```
60 /// use majordomus_cli::quality::Severity;
61 /// assert_eq!(Severity::Error.as_str(), "error");
62 /// ```
63 pub fn as_str(self) -> &'static str {
64 match self {
65 Severity::Error => "error",
66 Severity::Warning => "warning",
67 }
68 }
69}
70
71/// What is wrong, as a stable machine-readable code.
72///
73/// The codes are the contract between the validator and everything downstream: CI
74/// annotations, Cockpit filters, documentation anchors and an agent deciding what to fix.
75/// A code is never renamed once it has shipped; a rule that stops existing takes its code
76/// with it.
77#[derive(
78 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, schemars::JsonSchema,
79)]
80#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
81pub enum ViolationCode {
82 /// An exported item carries no documentation at all.
83 RustPublicMissingDocs,
84 /// An exported item's documentation says nothing its signature had not already said.
85 RustPublicThinDocs,
86 /// An exported item that carries behaviour has no executable example.
87 RustPublicMissingExample,
88 /// Every example an item has is `ignore`d or is prose in a fenced box.
89 RustExampleNotExecutable,
90 /// An example asserts nothing, or asserts something that is true of any program.
91 RustExamplePlaceholder,
92 /// An example never names the item it is documenting.
93 RustExampleDoesNotNameSubject,
94 /// An exported module carries no module-level documentation.
95 RustModuleMissingDocs,
96 /// An exported module has no module-level executable example.
97 RustModuleMissingExample,
98 /// No test names an exported module, and it declares none of its own.
99 RustModuleMissingBehaviouralTest,
100 /// A command of the command line is neither a canonical capability nor classified as
101 /// belonging to the command line alone.
102 OperationCliUnclassified,
103 /// A command is classified as belonging to the command line alone, and the command
104 /// line no longer has it.
105 OperationClassificationStale,
106 /// A command is classified as local *and* bound to a capability: two answers to one
107 /// question.
108 OperationClassificationConflict,
109 /// A capability declares an HTTP route and the OpenAPI document does not describe it.
110 OperationMissingOpenapi,
111 /// A capability declares a projection that the projection itself does not carry.
112 OperationProjectionMissing,
113 /// A projection carries an entry the registry does not hold.
114 OperationProjectionOrphan,
115}
116
117impl ViolationCode {
118 /// The code as it is written everywhere: `RUST_PUBLIC_MISSING_EXAMPLE`.
119 ///
120 /// ```
121 /// use majordomus_cli::quality::ViolationCode;
122 /// assert_eq!(ViolationCode::RustPublicMissingExample.as_str(), "RUST_PUBLIC_MISSING_EXAMPLE");
123 /// ```
124 pub fn as_str(self) -> &'static str {
125 match self {
126 ViolationCode::RustPublicMissingDocs => "RUST_PUBLIC_MISSING_DOCS",
127 ViolationCode::RustPublicThinDocs => "RUST_PUBLIC_THIN_DOCS",
128 ViolationCode::RustPublicMissingExample => "RUST_PUBLIC_MISSING_EXAMPLE",
129 ViolationCode::RustExampleNotExecutable => "RUST_EXAMPLE_NOT_EXECUTABLE",
130 ViolationCode::RustExamplePlaceholder => "RUST_EXAMPLE_PLACEHOLDER",
131 ViolationCode::RustExampleDoesNotNameSubject => "RUST_EXAMPLE_DOES_NOT_NAME_SUBJECT",
132 ViolationCode::RustModuleMissingDocs => "RUST_MODULE_MISSING_DOCS",
133 ViolationCode::RustModuleMissingExample => "RUST_MODULE_MISSING_EXAMPLE",
134 ViolationCode::RustModuleMissingBehaviouralTest => {
135 "RUST_MODULE_MISSING_BEHAVIOURAL_TEST"
136 }
137 ViolationCode::OperationCliUnclassified => "OPERATION_CLI_UNCLASSIFIED",
138 ViolationCode::OperationClassificationStale => "OPERATION_CLASSIFICATION_STALE",
139 ViolationCode::OperationClassificationConflict => "OPERATION_CLASSIFICATION_CONFLICT",
140 ViolationCode::OperationMissingOpenapi => "OPERATION_MISSING_OPENAPI",
141 ViolationCode::OperationProjectionMissing => "OPERATION_PROJECTION_MISSING",
142 ViolationCode::OperationProjectionOrphan => "OPERATION_PROJECTION_ORPHAN",
143 }
144 }
145
146 /// The rule of the repository's layer that requires this, by canonical id.
147 ///
148 /// A finding that could not name a rule would be an opinion. Every code names one, and
149 /// the suite proves the rule exists.
150 ///
151 /// ```
152 /// use majordomus_cli::quality::ViolationCode;
153 /// assert_eq!(
154 /// ViolationCode::RustModuleMissingExample.rule(),
155 /// "project.rust-public-api-quality@1"
156 /// );
157 /// assert_eq!(
158 /// ViolationCode::OperationCliUnclassified.rule(),
159 /// "project.operation-transport-parity@1"
160 /// );
161 /// ```
162 pub fn rule(self) -> &'static str {
163 match self {
164 ViolationCode::OperationCliUnclassified
165 | ViolationCode::OperationClassificationStale
166 | ViolationCode::OperationClassificationConflict
167 | ViolationCode::OperationMissingOpenapi
168 | ViolationCode::OperationProjectionMissing
169 | ViolationCode::OperationProjectionOrphan => "project.operation-transport-parity@1",
170 _ => "project.rust-public-api-quality@1",
171 }
172 }
173
174 /// Why it matters, in one sentence: what goes wrong in the world if this stands.
175 ///
176 /// ```
177 /// use majordomus_cli::quality::ViolationCode;
178 /// assert!(ViolationCode::RustExamplePlaceholder.why().contains("nothing"));
179 /// ```
180 pub fn why(self) -> &'static str {
181 match self {
182 ViolationCode::RustPublicMissingDocs => {
183 "an exported item with no documentation is API somebody has to read the implementation to use"
184 }
185 ViolationCode::RustPublicThinDocs => {
186 "documentation that repeats the signature costs a reader a lookup and tells them nothing"
187 }
188 ViolationCode::RustPublicMissingExample => {
189 "prose about behaviour drifts; an executable example cannot, because the toolchain runs it"
190 }
191 ViolationCode::RustExampleNotExecutable => {
192 "an ignored or non-Rust block is compiled by nobody, so it documents whatever the API used to be"
193 }
194 ViolationCode::RustExamplePlaceholder => {
195 "an example that asserts nothing proves nothing and exists only to satisfy a counter"
196 }
197 ViolationCode::RustExampleDoesNotNameSubject => {
198 "an example that never uses the item it documents is not evidence about that item"
199 }
200 ViolationCode::RustModuleMissingDocs => {
201 "a module is a conceptual boundary, and one that does not explain itself is a directory"
202 }
203 ViolationCode::RustModuleMissingExample => {
204 "a reader arriving at a module needs one worked lifecycle, not a list of items"
205 }
206 ViolationCode::RustModuleMissingBehaviouralTest => {
207 "a module nothing exercises is a module whose behaviour no change can break visibly"
208 }
209 ViolationCode::OperationCliUnclassified => {
210 "a command that is neither a capability nor declared command-line-only is missing from the API by accident rather than by decision"
211 }
212 ViolationCode::OperationClassificationStale => {
213 "a classification naming a command that no longer exists is a statement about nothing, and hides the next one that goes stale"
214 }
215 ViolationCode::OperationClassificationConflict => {
216 "a command cannot be both projected to the API and declared local; one of the two is wrong"
217 }
218 ViolationCode::OperationMissingOpenapi => {
219 "a route absent from the document is a route no client, no Swagger UI and no generated reference knows about"
220 }
221 ViolationCode::OperationProjectionMissing => {
222 "a declared projection that does not exist is a promise the descriptor makes and the process does not keep"
223 }
224 ViolationCode::OperationProjectionOrphan => {
225 "an entry no descriptor declares is a definition living in a projection, which is the duplication the registry exists to prevent"
226 }
227 }
228 }
229
230 /// What to do about it, in the imperative. Read by a person and by an agent.
231 ///
232 /// ```
233 /// use majordomus_cli::quality::ViolationCode;
234 /// assert!(ViolationCode::OperationCliUnclassified.remediation().contains("CliExposure"));
235 /// ```
236 pub fn remediation(self) -> &'static str {
237 match self {
238 ViolationCode::RustPublicMissingDocs => {
239 "write a doc comment saying what the item is for, what it does with its inputs, and what it may fail with"
240 }
241 ViolationCode::RustPublicThinDocs => {
242 "say what the signature cannot: the invariant, the side effect, the failure, or why the item exists"
243 }
244 ViolationCode::RustPublicMissingExample => {
245 "add a ``` block that calls the item and asserts the result; or reduce the item's visibility to pub(crate) if no consumer needs it"
246 }
247 ViolationCode::RustExampleNotExecutable => {
248 "drop `ignore` so the toolchain compiles the block, or use `no_run` when only running is undesirable"
249 }
250 ViolationCode::RustExamplePlaceholder => {
251 "assert something the item computes, not something true of every program"
252 }
253 ViolationCode::RustExampleDoesNotNameSubject => {
254 "call the documented item in the example, by the name a caller would write"
255 }
256 ViolationCode::RustModuleMissingDocs => {
257 "write a //! header: what the module owns, its invariants, its error model, and how it relates to its neighbours"
258 }
259 ViolationCode::RustModuleMissingExample => {
260 "add a ``` block to the //! header showing the module's normal lifecycle end to end"
261 }
262 ViolationCode::RustModuleMissingBehaviouralTest => {
263 "add a #[cfg(test)] test beside the declaration, or exercise the module from a test under tests/"
264 }
265 ViolationCode::OperationCliUnclassified => {
266 "give the capability a CliExposure so the command is the projection of one, or declare the command in cli::LOCAL with the reason it belongs to the command line alone"
267 }
268 ViolationCode::OperationClassificationStale => {
269 "remove the entry from cli::LOCAL: the command it names is gone"
270 }
271 ViolationCode::OperationClassificationConflict => {
272 "remove the cli::LOCAL entry, or the CliExposure; the command has an answer already"
273 }
274 ViolationCode::OperationMissingOpenapi => {
275 "rebuild the OpenAPI document from the registry rather than editing it; a route it lacks is a route the generator did not see"
276 }
277 ViolationCode::OperationProjectionMissing => {
278 "check the descriptor's exposure against the projection builder; nothing but the descriptor may decide this"
279 }
280 ViolationCode::OperationProjectionOrphan => {
281 "delete the entry from the projection and declare it on the capability, where every projection reads it"
282 }
283 }
284 }
285
286 /// How bad it is. Every code this subsystem ships is an error: a warning-forever mode
287 /// is how a rule becomes decoration.
288 pub fn severity(self) -> Severity {
289 Severity::Error
290 }
291
292 /// Every code, for the projections that list them and the test that proves each one is
293 /// distinct.
294 ///
295 /// ```
296 /// use majordomus_cli::quality::ViolationCode;
297 /// let all = ViolationCode::all();
298 /// let mut codes: Vec<&str> = all.iter().map(|c| c.as_str()).collect();
299 /// codes.sort();
300 /// codes.dedup();
301 /// assert_eq!(codes.len(), all.len(), "every code is distinct");
302 /// ```
303 pub fn all() -> Vec<ViolationCode> {
304 use ViolationCode::*;
305 vec![
306 RustPublicMissingDocs,
307 RustPublicThinDocs,
308 RustPublicMissingExample,
309 RustExampleNotExecutable,
310 RustExamplePlaceholder,
311 RustExampleDoesNotNameSubject,
312 RustModuleMissingDocs,
313 RustModuleMissingExample,
314 RustModuleMissingBehaviouralTest,
315 OperationCliUnclassified,
316 OperationClassificationStale,
317 OperationClassificationConflict,
318 OperationMissingOpenapi,
319 OperationProjectionMissing,
320 OperationProjectionOrphan,
321 ]
322 }
323}
324
325/// One finding: what, where, why, and what to do.
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
327pub struct Violation {
328 /// The stable code.
329 pub code: ViolationCode,
330 /// How bad it is.
331 pub severity: Severity,
332 /// The canonical id of the rule that requires this.
333 pub rule: String,
334 /// Where it is, repository-relative; empty when the finding is not about a file.
335 pub path: String,
336 /// The line, 1-based; `None` when the finding is not about a line.
337 #[serde(skip_serializing_if = "Option::is_none")]
338 pub line: Option<usize>,
339 /// The item's full path, or the command, or the capability id: what the finding is about.
340 pub symbol: String,
341 /// One sentence, specific to this occurrence.
342 pub message: String,
343 /// Why it matters, from the code.
344 pub why: String,
345 /// What to do, from the code.
346 pub remediation: String,
347}
348
349impl Violation {
350 /// A finding about an item at a place.
351 ///
352 /// The `why` and the `remediation` come from the code rather than from the caller, so
353 /// that two findings of one kind cannot explain themselves differently.
354 ///
355 /// ```
356 /// use majordomus_cli::quality::{Violation, ViolationCode};
357 /// let v = Violation::new(
358 /// ViolationCode::RustModuleMissingExample,
359 /// "majordomus_cli::graph",
360 /// "apps/majordomus-cli/src/graph.rs",
361 /// Some(1),
362 /// "the module header carries no executable example",
363 /// );
364 /// assert_eq!(v.rule, "project.rust-public-api-quality@1");
365 /// assert!(v.remediation.contains("//!"));
366 /// ```
367 pub fn new(
368 code: ViolationCode,
369 symbol: impl Into<String>,
370 path: impl Into<String>,
371 line: Option<usize>,
372 message: impl Into<String>,
373 ) -> Self {
374 Violation {
375 code,
376 severity: code.severity(),
377 rule: code.rule().to_string(),
378 path: path.into(),
379 line,
380 symbol: symbol.into(),
381 message: message.into(),
382 why: code.why().to_string(),
383 remediation: code.remediation().to_string(),
384 }
385 }
386
387 /// The finding as one line, the way the terminal and a CI annotation show it.
388 ///
389 /// ```
390 /// use majordomus_cli::quality::{Violation, ViolationCode};
391 /// let v = Violation::new(
392 /// ViolationCode::RustPublicMissingExample,
393 /// "majordomus_cli::graph::build",
394 /// "apps/majordomus-cli/src/graph.rs",
395 /// Some(42),
396 /// "carries behaviour and no executable example",
397 /// );
398 /// assert_eq!(
399 /// v.line_summary(),
400 /// "apps/majordomus-cli/src/graph.rs:42: RUST_PUBLIC_MISSING_EXAMPLE majordomus_cli::graph::build — carries behaviour and no executable example"
401 /// );
402 /// ```
403 pub fn line_summary(&self) -> String {
404 let at = match self.line {
405 Some(l) => format!("{}:{l}", self.path),
406 None if self.path.is_empty() => String::new(),
407 None => self.path.clone(),
408 };
409 let head = if at.is_empty() {
410 String::new()
411 } else {
412 format!("{at}: ")
413 };
414 format!(
415 "{head}{} {} — {}",
416 self.code.as_str(),
417 self.symbol,
418 self.message
419 )
420 }
421}
422
423/// What the exported item surface looks like.
424#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
425pub struct PublicApiQuality {
426 /// Items the crate exports, of every kind.
427 pub items: usize,
428 /// Of those, how many carry documentation.
429 pub documented: usize,
430 /// How many owe an executable example under the policy.
431 pub owe_example: usize,
432 /// Of those, how many have one that counts.
433 pub exampled: usize,
434 /// How many are exempt from the example policy for a structural reason, and why, by
435 /// reason, so that the exemptions are visible rather than implied by a subtraction.
436 pub exempt: Vec<Exemption>,
437}
438
439/// A group of items the example policy does not apply to, with the reason it does not.
440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
441pub struct Exemption {
442 /// The reason, as the policy names it.
443 pub reason: String,
444 /// How many items it covers.
445 pub items: usize,
446}
447
448/// What the exported module surface looks like.
449#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
450pub struct ModuleQuality {
451 /// Modules the crate exports, the crate root included.
452 pub modules: usize,
453 /// Of those, how many carry a `//!` header.
454 pub documented: usize,
455 /// How many carry an executable example in that header.
456 pub exampled: usize,
457 /// How many something exercises: an in-file test, or a test that names them.
458 pub behaviourally_tested: usize,
459}
460
461/// What the canonical operations look like against the transports that project them.
462#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
463pub struct OperationParity {
464 /// Executable capabilities in the registry.
465 pub canonical: usize,
466 /// Of those, how many declare a command-line projection.
467 pub cli: usize,
468 /// How many declare an HTTP route.
469 pub http: usize,
470 /// How many the OpenAPI document describes.
471 pub openapi: usize,
472 /// How many declare an MCP tool.
473 pub mcp: usize,
474 /// Commands of the command line, leaves and runnable parents.
475 pub cli_commands: usize,
476 /// Of those, how many are the projection of a capability.
477 pub cli_from_capability: usize,
478 /// How many are declared to belong to the command line alone, with a reason.
479 pub cli_local: usize,
480}
481
482/// One measurement of one crate, and everything found wrong in it.
483#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
484pub struct QualityReport {
485 /// [`SCHEMA`].
486 #[serde(default)]
487 pub schema: String,
488 /// What was measured: the crate directory, repository-relative.
489 pub target: String,
490 /// The exported item surface.
491 pub public_api: PublicApiQuality,
492 /// The exported module surface.
493 pub modules: ModuleQuality,
494 /// The canonical operations against their transports.
495 pub operations: OperationParity,
496 /// Every finding, errors first, then by file and line.
497 pub violations: Vec<Violation>,
498}
499
500impl QualityReport {
501 /// Nothing wrong at any severity that blocks?
502 ///
503 /// ```
504 /// use majordomus_cli::quality::{QualityReport, Violation, ViolationCode};
505 /// let mut r = QualityReport::default();
506 /// assert!(r.passes());
507 /// r.violations.push(Violation::new(
508 /// ViolationCode::RustModuleMissingDocs, "m", "src/m.rs", Some(1), "no header"));
509 /// assert!(!r.passes());
510 /// ```
511 pub fn passes(&self) -> bool {
512 self.count(Severity::Error) == 0
513 }
514
515 /// How many findings of a severity.
516 pub fn count(&self, severity: Severity) -> usize {
517 self.violations
518 .iter()
519 .filter(|v| v.severity == severity)
520 .count()
521 }
522
523 /// The process exit code: `0` clean, `10` contract unmet, which is the code every
524 /// other unmet contract in this executable uses.
525 ///
526 /// ```
527 /// use majordomus_cli::quality::{QualityReport, Violation, ViolationCode};
528 /// let mut r = QualityReport::default();
529 /// assert_eq!(r.exit_code(), 0);
530 /// r.violations.push(Violation::new(
531 /// ViolationCode::OperationCliUnclassified, "majordomus x", "", None, "unclassified"));
532 /// assert_eq!(r.exit_code(), 10);
533 /// ```
534 pub fn exit_code(&self) -> u8 {
535 if self.passes() {
536 0
537 } else {
538 10
539 }
540 }
541
542 /// Findings of one code, in report order.
543 ///
544 /// ```
545 /// use majordomus_cli::quality::{QualityReport, Violation, ViolationCode};
546 /// let mut r = QualityReport::default();
547 /// r.violations.push(Violation::new(
548 /// ViolationCode::RustModuleMissingExample, "m", "src/m.rs", Some(1), "none"));
549 /// assert_eq!(r.of(ViolationCode::RustModuleMissingExample).len(), 1);
550 /// assert!(r.of(ViolationCode::RustModuleMissingDocs).is_empty());
551 /// ```
552 pub fn of(&self, code: ViolationCode) -> Vec<&Violation> {
553 self.violations.iter().filter(|v| v.code == code).collect()
554 }
555
556 /// Sort the findings the way every rendering shows them: errors first, then by file,
557 /// then by line, then by code, so that two runs over one tree print the same report.
558 pub fn sort(&mut self) {
559 self.violations.sort_by(|a, b| {
560 a.severity
561 .cmp(&b.severity)
562 .then_with(|| a.path.cmp(&b.path))
563 .then_with(|| a.line.cmp(&b.line))
564 .then_with(|| a.code.cmp(&b.code))
565 .then_with(|| a.symbol.cmp(&b.symbol))
566 });
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn every_code_names_a_rule_a_reason_and_a_remedy() {
576 for code in ViolationCode::all() {
577 assert!(code.rule().starts_with("project."), "{code:?}");
578 assert!(code.why().len() > 30, "{code:?} explains nothing");
579 assert!(
580 code.remediation().len() > 20,
581 "{code:?} tells nobody what to do"
582 );
583 // the code as text is the enum name in screaming snake, and round-trips
584 let json = serde_json::to_string(&code).unwrap();
585 assert_eq!(json, format!("\"{}\"", code.as_str()));
586 assert_eq!(serde_json::from_str::<ViolationCode>(&json).unwrap(), code);
587 }
588 }
589
590 #[test]
591 fn a_report_with_a_finding_fails_and_says_so_in_both_renderings() {
592 let mut r = QualityReport::default();
593 r.violations.push(Violation::new(
594 ViolationCode::RustPublicMissingExample,
595 "majordomus_cli::a::b",
596 "apps/majordomus-cli/src/a.rs",
597 Some(7),
598 "carries behaviour and no executable example",
599 ));
600 assert!(!r.passes());
601 assert_eq!(r.exit_code(), 10);
602 // the JSON and the human rendering agree about the outcome: never one without the other
603 let json = serde_json::to_value(&r).unwrap();
604 assert_eq!(json["violations"][0]["code"], "RUST_PUBLIC_MISSING_EXAMPLE");
605 assert_eq!(
606 json["violations"][0]["rule"],
607 "project.rust-public-api-quality@1"
608 );
609 assert!(r.violations[0].line_summary().contains("src/a.rs:7"));
610 }
611
612 #[test]
613 fn the_order_of_findings_does_not_depend_on_the_order_they_were_found() {
614 let v = |path: &str, line: usize| {
615 Violation::new(
616 ViolationCode::RustModuleMissingDocs,
617 "m",
618 path,
619 Some(line),
620 "none",
621 )
622 };
623 let mut a = QualityReport {
624 violations: vec![v("b.rs", 2), v("a.rs", 9), v("a.rs", 1)],
625 ..QualityReport::default()
626 };
627 let mut b = QualityReport {
628 violations: vec![v("a.rs", 1), v("b.rs", 2), v("a.rs", 9)],
629 ..QualityReport::default()
630 };
631 a.sort();
632 b.sort();
633 assert_eq!(a.violations, b.violations);
634 assert_eq!(a.violations[0].path, "a.rs");
635 assert_eq!(a.violations[0].line, Some(1));
636 }
637}