codehelion_helper_protocol/protocol.rs
1//! The wire format core and helpers agree on.
2//!
3//! Everything here is data: no process is spawned and no compiler is linked.
4//! A helper binary depends on this module to speak the protocol and on nothing
5//! else of codehelion, which is what keeps a toolchain dependency from reaching
6//! the analysis crates.
7//!
8//! # Framing
9//!
10//! Messages travel over the helper's standard input and output as
11//! length-prefixed frames: a four-byte big-endian payload length, a one-byte
12//! encoding tag, then the payload. The tag exists so that compiler IR too large
13//! to be worth serializing as text can travel as bytes later without changing
14//! how a frame is found in the stream — only [`Encoding`] gains a variant.
15//!
16//! A frame carries its own length so a reader never has to guess where a
17//! message ends, and never has to trust the sender's promise about total
18//! volume: [`MAX_FRAME_BYTES`] bounds what one frame may ask a reader to
19//! allocate, so a helper that has gone wrong cannot exhaust memory before it is
20//! noticed.
21
22use std::io::{Read, Write};
23
24use serde::{Deserialize, Serialize};
25
26use crate::ir::{CompilerIr, Unavailability, UnitRef};
27
28/// The only protocol revision this build speaks.
29///
30/// The product has not been released, so clients and helpers use the complete
31/// current protocol directly.
32pub const PROTOCOL_VERSION: u32 = 1;
33
34/// Largest payload a single frame may declare.
35///
36/// A reader allocates what the header says before it has seen the body, so
37/// this is the ceiling on what one malformed or hostile frame can cost.
38pub const MAX_FRAME_BYTES: u32 = 64 * 1024 * 1024;
39
40/// How a frame's payload is encoded.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43#[repr(u8)]
44pub enum Encoding {
45 /// UTF-8 JSON. The only encoding this revision writes.
46 Json = 0,
47}
48
49impl Encoding {
50 /// The tag byte written into the frame header.
51 #[must_use]
52 pub const fn tag(self) -> u8 {
53 self as u8
54 }
55
56 /// The encoding a tag byte names, or `None` if this build has no such
57 /// encoding — which is how a frame from a newer peer is refused rather
58 /// than misread.
59 #[must_use]
60 pub const fn from_tag(tag: u8) -> Option<Self> {
61 match tag {
62 0 => Some(Self::Json),
63 _ => None,
64 }
65 }
66}
67
68/// Something a helper can be asked for.
69///
70/// A helper reports the subset it can supply during the handshake, and a run
71/// asks for nothing outside that subset. The variants are the information
72/// kinds semantic analysis is built from; a helper that offers fewer is not
73/// broken, it is less capable, and [`Capability::absence`] says what that costs.
74#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum Capability {
77 /// Resolved types for expressions and bindings.
78 Types,
79 /// Which definition each name refers to, and whether it is outside the
80 /// scanned code.
81 NameResolution,
82 /// Resolved call targets.
83 CallTargets,
84 /// A control-flow graph built from the compiler's own.
85 MirCfg,
86 /// Macro expansion with both spelling and expansion locations.
87 MacroExpansion,
88 /// Template or generic instantiation traced to its definition.
89 TemplateInstantiation,
90}
91
92/// What a run does when a helper cannot supply a capability.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Absence {
95 /// Continue without it, recording what was not available.
96 Degrade,
97 /// Refuse semantic analysis: without this the result would be a weaker
98 /// answer wearing a stronger name.
99 Refuse,
100}
101
102impl Capability {
103 /// Every capability this protocol revision understands.
104 pub const ALL: [Self; 6] = [
105 Self::Types,
106 Self::NameResolution,
107 Self::CallTargets,
108 Self::MirCfg,
109 Self::MacroExpansion,
110 Self::TemplateInstantiation,
111 ];
112
113 /// Stable lowercase identifier, the same spelling this serializes as.
114 #[must_use]
115 pub const fn name(self) -> &'static str {
116 match self {
117 Self::Types => "types",
118 Self::NameResolution => "name_resolution",
119 Self::CallTargets => "call_targets",
120 Self::MirCfg => "mir_cfg",
121 Self::MacroExpansion => "macro_expansion",
122 Self::TemplateInstantiation => "template_instantiation",
123 }
124 }
125
126 /// What its absence costs.
127 ///
128 /// Two are load-bearing, for the same reason stated twice. Semantic mode
129 /// exists to answer with what the compiler knows rather than with what the
130 /// text looks like: without resolved types a run reports syntactic findings
131 /// under a semantic label, and without name resolution it decides which
132 /// names to compare on by guessing from their spelling, which is the
133 /// structural answer wearing the same stronger name.
134 ///
135 /// Everything else refines an answer those two make possible in the first
136 /// place, so missing any of them narrows the result rather than misnaming
137 /// it.
138 #[must_use]
139 pub const fn absence(self) -> Absence {
140 match self {
141 Self::Types | Self::NameResolution => Absence::Refuse,
142 Self::CallTargets
143 | Self::MirCfg
144 | Self::MacroExpansion
145 | Self::TemplateInstantiation => Absence::Degrade,
146 }
147 }
148}
149
150/// A message from core to a helper.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct Request {
153 /// The revision this message is written in.
154 pub protocol_version: u32,
155 /// Correlates a response with the request that asked for it.
156 pub id: u64,
157 /// What is being asked.
158 pub body: RequestBody,
159}
160
161/// The askable things.
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(tag = "kind", rename_all = "snake_case")]
164pub enum RequestBody {
165 /// Identify yourself and say what you can do.
166 Handshake(ClientIdentity),
167 /// Say what the code in a tree is analyzed under.
168 ///
169 /// Asked before any unit is, because what a run records its answers under
170 /// has to be settled before there are answers to record.
171 DescribeBuild(DescribeBuild),
172 /// Analyze one unit and return what the compiler knows about it.
173 Analyze(Analyze),
174 /// Finish outstanding work and exit.
175 Shutdown,
176}
177
178/// The tree whose build is being asked about.
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub struct DescribeBuild {
181 /// A directory inside the project, as this machine spells it.
182 ///
183 /// Not necessarily the project's own root: a scan can be rooted at one
184 /// member of a workspace, and finding the project from there is the
185 /// helper's job because it is the side that knows what a project is.
186 pub root: String,
187}
188
189/// The conditions a tree's code is analyzed under.
190///
191/// What belongs here is what changes the answers rather than what changes the
192/// build: two runs that resolve the same names to the same things are one
193/// variant however differently they were invoked, and two that do not are two
194/// however alike the command line looked.
195///
196/// Empty on both counts when the helper found no project to describe. That is
197/// not the same claim as a project that enables nothing — a described build
198/// always has settings, because the target alone supplies a dozen — so nothing
199/// has to be spelled to tell the two apart.
200#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
201pub struct BuildDescription {
202 /// Enabled features, each qualified by the package that enables it.
203 ///
204 /// Qualified because a feature is declared per package: `serde/derive` and
205 /// `ledger/derive` are unrelated facts, and an unqualified list would let
206 /// one package's selection stand in for another's.
207 pub features: Vec<String>,
208 /// The conditional-compilation settings the code is read under, as the
209 /// compiler spells them.
210 pub cfgs: Vec<String>,
211}
212
213/// One unit to analyze, and what is wanted from it.
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
215pub struct Analyze {
216 /// Which unit.
217 pub unit: UnitRef,
218 /// The exact compilation-database entry to use for a C or C++ request.
219 ///
220 /// A source path is not enough: a database may intentionally list it more
221 /// than once under different `-D` settings. This carries the complete
222 /// recorded command identity rather than a database index, because an
223 /// index changes when an unrelated command is inserted or reordered.
224 pub compile_command: Option<CompileCommandSelector>,
225 /// Canonical scan-root boundary for paths a compilation command may read.
226 ///
227 /// Set only for an untrusted scan. Helpers must refuse path-bearing
228 /// compiler arguments that resolve outside this directory; omitting it
229 /// preserves the configured, trusted compilation-database behaviour.
230 pub read_boundary: Option<String>,
231 /// What to spend time on.
232 ///
233 /// Never more than the helper offered at handshake. Asking for less than it
234 /// can do is how a run that needs only types avoids paying for a
235 /// control-flow graph nobody will read.
236 pub want: Vec<Capability>,
237 /// What the helper may run out of the project while answering.
238 ///
239 /// Empty unless somebody said otherwise.
240 pub permitted: Vec<Execution>,
241}
242
243/// One stable, exact selector for an entry in `compile_commands.json`.
244///
245/// The path is the entry's source after resolving it against `directory`; the
246/// command remains one argument per element so quoting cannot alter its
247/// meaning between the scanner and helper. Together they name one database
248/// entry without relying on its position in a generated file.
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
250pub struct CompileCommandSelector {
251 /// The translation-unit source path.
252 pub file: String,
253 /// The command's working directory, when the database recorded one.
254 pub directory: Option<String>,
255 /// The recorded compiler invocation, including its compiler and source.
256 pub arguments: Vec<String>,
257}
258
259impl CompileCommandSelector {
260 /// Whether this and `other` name one entry of one compilation database.
261 ///
262 /// Not derived equality, because the two are built by two programs out of
263 /// one database and their paths are two resolvings of one file. Those are
264 /// compared as paths, and past a Windows verbatim prefix that only one of
265 /// the two need have come back carrying — where they are compared as
266 /// strings instead, no entry matches any request and every unit of a C or
267 /// C++ project comes back with no build information.
268 ///
269 /// The arguments are compared exactly: they are the words the database
270 /// recorded, which neither side resolved and neither side may reword.
271 #[must_use]
272 pub fn names_the_same_entry(&self, other: &Self) -> bool {
273 fn one_path(left: &str, right: &str) -> bool {
274 crate::ir::ordinary(std::path::Path::new(left))
275 == crate::ir::ordinary(std::path::Path::new(right))
276 }
277 self.arguments == other.arguments
278 && one_path(&self.file, &other.file)
279 && match (self.directory.as_deref(), other.directory.as_deref()) {
280 (Some(mine), Some(theirs)) => one_path(mine, theirs),
281 (None, None) => true,
282 _ => false,
283 }
284 }
285}
286
287/// Something a helper may be permitted to run out of the project it is
288/// analyzing.
289///
290/// Named per class rather than as one switch, for the reason the tool's own
291/// permissions are: expanding a macro the project's developers already run and
292/// executing a configure step that may reach the network are decisions of
293/// different weight, and a single permission would make agreeing to either mean
294/// agreeing to both.
295#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
296#[serde(rename_all = "kebab-case")]
297pub enum Execution {
298 /// A Cargo build script.
299 BuildScript,
300 /// A procedural macro, expanded by compiling and calling it.
301 ProcMacro,
302 /// A configure step: `CMake`, autotools, or a generator script.
303 Configure,
304 /// A compiler wrapper the project interposes.
305 CompilerWrapper,
306 /// A command that generates source files.
307 GeneratedSource,
308}
309
310impl Execution {
311 /// Stable identifier, the same spelling this serializes as and the same
312 /// one a person types to permit it.
313 #[must_use]
314 pub const fn name(self) -> &'static str {
315 match self {
316 Self::BuildScript => "build-script",
317 Self::ProcMacro => "proc-macro",
318 Self::Configure => "configure",
319 Self::CompilerWrapper => "compiler-wrapper",
320 Self::GeneratedSource => "generated-source",
321 }
322 }
323
324 /// The class a name refers to, or `None` for one this build cannot name.
325 #[must_use]
326 pub fn from_name(name: &str) -> Option<Self> {
327 [
328 Self::BuildScript,
329 Self::ProcMacro,
330 Self::Configure,
331 Self::CompilerWrapper,
332 Self::GeneratedSource,
333 ]
334 .into_iter()
335 .find(|class| class.name() == name)
336 }
337}
338
339/// Who is connecting, and which revisions it can speak.
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341pub struct ClientIdentity {
342 /// Name of the connecting program.
343 pub client: String,
344 /// Its version, for diagnostics rather than for negotiation.
345 pub client_version: String,
346 /// The exact protocol revision it speaks.
347 pub protocol: u32,
348}
349
350/// A message from a helper to core.
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352pub struct Response {
353 /// The revision this message is written in.
354 pub protocol_version: u32,
355 /// The request this answers.
356 pub id: u64,
357 /// The answer.
358 pub body: ResponseBody,
359}
360
361/// The answers.
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(tag = "kind", rename_all = "snake_case")]
364pub enum ResponseBody {
365 /// Who the helper is and what it can do.
366 Handshake(Box<HelperIdentity>),
367 /// What the tree's code is analyzed under.
368 Build(Box<BuildDescription>),
369 /// What the compiler knows about the unit.
370 Analyzed(Box<CompilerIr>),
371 /// Nothing can be known about the unit, and why.
372 ///
373 /// Distinct from [`ResponseBody::Failed`]: the helper is working, and this
374 /// unit is one it cannot analyze. A scan carries on and says so.
375 Unavailable {
376 /// Which unit.
377 unit: UnitRef,
378 /// Why it cannot be analyzed.
379 reason: Unavailability,
380 },
381 /// Shutdown acknowledged.
382 Shutdown,
383 /// The request could not be answered.
384 Failed(Failure),
385}
386
387/// What a helper says about itself.
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
389pub struct HelperIdentity {
390 /// Helper name, as `doctor` reports it.
391 pub name: String,
392 /// Helper version.
393 pub version: String,
394 /// The exact protocol revision it speaks.
395 pub protocol: u32,
396 /// The toolchains it was built against, as the compiler spells them.
397 ///
398 /// A helper built for one compiler release cannot be trusted against
399 /// another, so this is matched against the project's own toolchain rather
400 /// than assumed compatible.
401 pub toolchains: Vec<String>,
402 /// What it can supply.
403 pub capabilities: Vec<Capability>,
404 /// The classes of execution it will act on when it is permitted them.
405 ///
406 /// Stated so that permitting something this helper would not do can be
407 /// refused rather than accepted and forgotten. A permission that changes
408 /// nothing is worse than one that is turned down: somebody granted it, and
409 /// the thin answer that follows looks like the project's own.
410 pub executes: Vec<Execution>,
411}
412
413/// Why a request could not be answered.
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct Failure {
416 /// A stable short code for programmatic handling.
417 pub code: String,
418 /// What went wrong, for a person.
419 pub message: String,
420}
421
422/// Something that went wrong reading or writing a frame.
423#[derive(Debug, thiserror::Error)]
424pub enum FrameError {
425 /// The stream ended where a frame was expected.
426 #[error("the stream ended mid-frame")]
427 Truncated,
428 /// The header named a payload larger than [`MAX_FRAME_BYTES`].
429 #[error("a frame declared {declared} bytes, over the {MAX_FRAME_BYTES} ceiling")]
430 TooLarge {
431 /// What the header claimed.
432 declared: u32,
433 },
434 /// The header named an encoding this build does not have.
435 #[error("a frame arrived in unknown encoding {tag}")]
436 UnknownEncoding {
437 /// The tag byte that was read.
438 tag: u8,
439 },
440 /// The payload was not the message it claimed to be.
441 #[error("a frame's payload did not parse: {0}")]
442 Malformed(#[from] serde_json::Error),
443 /// The underlying stream failed.
444 #[error("the stream failed: {0}")]
445 Io(#[from] std::io::Error),
446}
447
448/// Header length: four bytes of payload length plus one encoding tag.
449const HEADER_BYTES: usize = 5;
450
451/// Write `value` as one JSON frame.
452///
453/// # Errors
454///
455/// Fails if the value cannot be serialized or the stream cannot take it.
456pub fn write_frame<W: Write, T: Serialize>(writer: &mut W, value: &T) -> Result<(), FrameError> {
457 let payload = serde_json::to_vec(value)?;
458 let length =
459 u32::try_from(payload.len()).map_err(|_| FrameError::TooLarge { declared: u32::MAX })?;
460 if length > MAX_FRAME_BYTES {
461 return Err(FrameError::TooLarge { declared: length });
462 }
463 let mut header = [0u8; HEADER_BYTES];
464 header[..4].copy_from_slice(&length.to_be_bytes());
465 header[4] = Encoding::Json.tag();
466 writer.write_all(&header)?;
467 writer.write_all(&payload)?;
468 writer.flush()?;
469 Ok(())
470}
471
472/// Read one frame and parse it.
473///
474/// Returns `Ok(None)` when the stream ends cleanly between frames, which is how
475/// a peer that has finished is told apart from one that died mid-message.
476///
477/// # Errors
478///
479/// Fails on a truncated frame, an oversized or unknown-encoding header, a
480/// payload that does not parse, or a stream error.
481pub fn read_frame<R: Read, T: for<'de> Deserialize<'de>>(
482 reader: &mut R,
483) -> Result<Option<T>, FrameError> {
484 let mut header = [0u8; HEADER_BYTES];
485 match read_exact_or_eof(reader, &mut header)? {
486 Read0::Eof => return Ok(None),
487 Read0::Partial => return Err(FrameError::Truncated),
488 Read0::Full => {}
489 }
490 let length = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
491 if length > MAX_FRAME_BYTES {
492 return Err(FrameError::TooLarge { declared: length });
493 }
494 if Encoding::from_tag(header[4]).is_none() {
495 return Err(FrameError::UnknownEncoding { tag: header[4] });
496 }
497 let mut payload = vec![0u8; length as usize];
498 match read_exact_or_eof(reader, &mut payload)? {
499 Read0::Full => {}
500 Read0::Eof | Read0::Partial => return Err(FrameError::Truncated),
501 }
502 Ok(Some(serde_json::from_slice(&payload)?))
503}
504
505/// How much of a buffer a read managed to fill.
506enum Read0 {
507 /// Nothing at all: the stream ended between frames.
508 Eof,
509 /// Some but not all: the stream ended inside a frame.
510 Partial,
511 /// All of it.
512 Full,
513}
514
515fn read_exact_or_eof<R: Read>(reader: &mut R, buffer: &mut [u8]) -> Result<Read0, std::io::Error> {
516 let mut filled = 0;
517 while filled < buffer.len() {
518 let read = reader.read(&mut buffer[filled..])?;
519 if read == 0 {
520 return Ok(if filled == 0 {
521 Read0::Eof
522 } else {
523 Read0::Partial
524 });
525 }
526 filled += read;
527 }
528 Ok(Read0::Full)
529}
530
531#[cfg(test)]
532#[allow(clippy::expect_used, clippy::unwrap_used)]
533mod tests {
534 use super::*;
535
536 fn identity() -> HelperIdentity {
537 HelperIdentity {
538 name: "mock".into(),
539 version: "0.1.0".into(),
540 protocol: PROTOCOL_VERSION,
541 toolchains: vec!["rustc 1.85.0".into()],
542 capabilities: vec![Capability::Types, Capability::CallTargets],
543 executes: vec![Execution::BuildScript],
544 }
545 }
546
547 #[test]
548 fn a_frame_survives_the_round_trip() {
549 let message = Response {
550 protocol_version: PROTOCOL_VERSION,
551 id: 7,
552 body: ResponseBody::Handshake(Box::new(identity())),
553 };
554 let mut buffer = Vec::new();
555 write_frame(&mut buffer, &message).unwrap();
556 let back: Option<Response> = read_frame(&mut buffer.as_slice()).unwrap();
557 assert_eq!(back, Some(message));
558 }
559
560 #[test]
561 fn frames_are_read_one_at_a_time_from_one_stream() {
562 let mut buffer = Vec::new();
563 for id in 0..3u64 {
564 write_frame(
565 &mut buffer,
566 &Response {
567 protocol_version: PROTOCOL_VERSION,
568 id,
569 body: ResponseBody::Shutdown,
570 },
571 )
572 .unwrap();
573 }
574 let mut stream = buffer.as_slice();
575 for id in 0..3u64 {
576 let message: Response = read_frame(&mut stream).unwrap().unwrap();
577 assert_eq!(message.id, id);
578 }
579 assert!(read_frame::<_, Response>(&mut stream).unwrap().is_none());
580 }
581
582 #[test]
583 fn a_stream_that_ends_between_frames_is_not_an_error() {
584 let empty: &[u8] = &[];
585 assert!(read_frame::<_, Response>(&mut { empty }).unwrap().is_none());
586 }
587
588 #[test]
589 fn a_stream_that_ends_inside_a_frame_is_an_error() {
590 let mut buffer = Vec::new();
591 write_frame(
592 &mut buffer,
593 &Response {
594 protocol_version: PROTOCOL_VERSION,
595 id: 1,
596 body: ResponseBody::Shutdown,
597 },
598 )
599 .unwrap();
600 buffer.truncate(buffer.len() - 1);
601 let error = read_frame::<_, Response>(&mut buffer.as_slice()).unwrap_err();
602 assert!(matches!(error, FrameError::Truncated), "{error:?}");
603 }
604
605 #[test]
606 fn an_oversized_header_is_refused_before_anything_is_allocated() {
607 let mut header = [0u8; HEADER_BYTES];
608 header[..4].copy_from_slice(&(MAX_FRAME_BYTES + 1).to_be_bytes());
609 let error = read_frame::<_, Response>(&mut header.as_slice()).unwrap_err();
610 assert!(matches!(error, FrameError::TooLarge { .. }), "{error:?}");
611 }
612
613 #[test]
614 fn an_encoding_this_build_lacks_is_refused_rather_than_guessed() {
615 let mut buffer = Vec::new();
616 write_frame(
617 &mut buffer,
618 &Response {
619 protocol_version: PROTOCOL_VERSION,
620 id: 1,
621 body: ResponseBody::Shutdown,
622 },
623 )
624 .unwrap();
625 buffer[4] = 9;
626 let error = read_frame::<_, Response>(&mut buffer.as_slice()).unwrap_err();
627 assert!(
628 matches!(error, FrameError::UnknownEncoding { tag: 9 }),
629 "{error:?}"
630 );
631 }
632
633 /// One spelling, kept in one place. A stored capability and a transmitted
634 /// one that disagree would make a database written by this build unreadable
635 /// by it.
636 #[test]
637 fn what_a_capability_is_called_is_what_it_is_sent_as() {
638 for capability in Capability::ALL {
639 let sent = serde_json::to_string(&capability).unwrap();
640 assert_eq!(sent, format!("\"{}\"", capability.name()));
641 }
642 }
643
644 /// The same rule as for capabilities, and for the same reason: what is
645 /// stored, what is typed and what is sent are one spelling.
646 #[test]
647 fn what_an_execution_class_is_called_is_what_it_is_sent_as() {
648 for class in [
649 Execution::BuildScript,
650 Execution::ProcMacro,
651 Execution::Configure,
652 Execution::CompilerWrapper,
653 Execution::GeneratedSource,
654 ] {
655 let sent = serde_json::to_string(&class).unwrap();
656 assert_eq!(sent, format!("\"{}\"", class.name()));
657 }
658 }
659
660 /// A name nobody recognises is not a class. Reading it as the catch-all
661 /// would let a misspelling travel as a permission, and the whole point of
662 /// naming classes is that granting one grants exactly one.
663 #[test]
664 fn a_class_nobody_can_name_is_not_read_as_the_one_with_no_name() {
665 assert_eq!(
666 Execution::from_name("build-script"),
667 Some(Execution::BuildScript)
668 );
669 assert_eq!(Execution::from_name("build-scripts"), None);
670 assert_eq!(Execution::from_name("unknown"), None);
671 assert!(
672 serde_json::from_str::<Vec<Execution>>(r#"["build-script","something-newer"]"#)
673 .is_err()
674 );
675 }
676
677 #[test]
678 fn a_capability_this_build_cannot_name_is_rejected() {
679 assert!(
680 serde_json::from_str::<Vec<Capability>>(r#"["types","overload_resolution"]"#).is_err()
681 );
682 }
683
684 /// The two that decide what a comparison is made of. Everything else
685 /// sharpens a comparison that these make possible at all.
686 #[test]
687 fn what_a_comparison_is_made_of_is_worth_refusing_over() {
688 assert_eq!(Capability::Types.absence(), Absence::Refuse);
689 assert_eq!(Capability::NameResolution.absence(), Absence::Refuse);
690 for capability in [
691 Capability::CallTargets,
692 Capability::MirCfg,
693 Capability::MacroExpansion,
694 Capability::TemplateInstantiation,
695 ] {
696 assert_eq!(capability.absence(), Absence::Degrade, "{capability:?}");
697 }
698 }
699
700 fn selector(file: &str, directory: Option<&str>) -> CompileCommandSelector {
701 CompileCommandSelector {
702 file: file.to_owned(),
703 directory: directory.map(ToOwned::to_owned),
704 arguments: vec!["clang++".into(), "-c".into(), "a.cpp".into()],
705 }
706 }
707
708 /// The scanner and the helper each resolve the database's paths for
709 /// themselves, and one of them coming back with the verbatim form is a
710 /// difference in how the path was written down rather than in which file
711 /// it names.
712 #[test]
713 fn one_entry_resolved_by_two_programs_is_one_entry() {
714 let plain = selector("C:/w/a.cpp", Some("C:/w"));
715 let verbatim = selector(r"\\?\C:/w/a.cpp", Some(r"\\?\C:/w"));
716 assert!(plain.names_the_same_entry(&verbatim));
717 assert!(verbatim.names_the_same_entry(&plain));
718 assert!(plain.names_the_same_entry(&plain));
719 }
720
721 /// A database may list one source more than once under different settings,
722 /// which is the whole reason a selector carries the command.
723 #[test]
724 fn two_commands_over_one_source_are_two_entries() {
725 let mut other = selector("C:/w/a.cpp", Some("C:/w"));
726 other.arguments.push("-DWIDE".into());
727 assert!(!selector("C:/w/a.cpp", Some("C:/w")).names_the_same_entry(&other));
728 assert!(
729 !selector("C:/w/a.cpp", Some("C:/w"))
730 .names_the_same_entry(&selector("C:/w/b.cpp", Some("C:/w")))
731 );
732 assert!(
733 !selector("C:/w/a.cpp", Some("C:/w"))
734 .names_the_same_entry(&selector("C:/w/a.cpp", None))
735 );
736 }
737}