git_policy_cli/push/push_failure_notice.rs
1//! What:
2//! The record of a backup push that failed after its commit was saved, and the rule for when the wrapper shows it:
3//! the resident service writes one record per failed push under the Git directory of the worktree whose commit it was, a
4//! later wrapper command in that worktree shows each record once on standard error, and a later push of the same branch
5//! that succeeds removes the records of that branch.
6//! Why:
7//! The push runs in the service after the commit command returned, so nothing is attached to the command that could
8//! print its failure; without a record the person would never learn that their backup push was refused
9//! (`doc/planning/cli-git-resident-service.md`, "After-commit work"). Records are plain files in the worktree's
10//! own Git directory so the check on every command is one directory read that finds nothing when nothing failed. A record holds
11//! the branch, the whole commit name and Git's words with credentials already masked; the wrapper words the notice when it
12//! shows it, so the commit name is abbreviated the way Git abbreviates it in the repository as it is then. Records are
13//! shown on standard error only, and not at all to a command whose output a program reads (`--porcelain`, `-z`,
14//! plumbing), so that program neither sees the text in its output nor consumes the record before a person can read it.
15//!
16//! In TS you'd write (pseudocode):
17//! ```ts
18//! await recordPushFailure(gitDir, { branchRef, oid, reason }); stderr.write(await takePushFailures(gitDir));
19//! ```
20
21use crate::auto_push::PUSH_FAILED_NOTE;
22use crate::auto_push_branch_key::encode_branch_key;
23use crate::lfs_output_mask::mask_userinfo_text;
24use crate::private_storage::{create_private_directory_all, write_private_file_unsynced};
25use crate::service_messages::RESTART_COMMAND;
26use std::ffi::OsString;
27use std::path::{Path, PathBuf};
28
29/// Directory under the worktree's own Git directory that holds the records for the person.
30pub const NOTICE_DIRECTORY: &str = "cli-git/notices";
31
32/// Directory under the repository's common Git directory that holds the records of removed worktrees, which any worktree's
33/// command shows because the worktree they were made for is gone.
34pub const ORPHAN_NOTICE_DIRECTORY: &str = "cli-git/orphaned-notices";
35
36/// First line of a record of a push that was lost with its worktree; its fields are those of a failure record, without words.
37const LOST_MAGIC: &str = "cli-git push lost 1";
38
39/// Separates the parts of a record's name; it is not a character a branch key can contain.
40const NAME_SEPARATOR: char = '+';
41
42/// The longest branch key a record name carries whole; a longer key is cut and tagged with a hash of the whole key, so the
43/// name stays a valid file name and two long branch names do not share records.
44/// Exempt from a citation under `LQM` (class: display cap): it limits how much text a message, log field or record
45/// names, and no command result or duration depends on it. Classed once in `doc/planning/cli-git-shipped-limits-
46/// audit.md`, "Exempt limits"; the coordinator's pick under the owner's delegation, open to the owner's veto.
47const LONGEST_KEY: usize = 100;
48
49/// The most bytes of Git's own words a record keeps.
50/// Exempt from a citation under `LQM` (class: display cap): it limits how much text a message, log field or record
51/// names, and no command result or duration depends on it. Classed once in `doc/planning/cli-git-shipped-limits-
52/// audit.md`, "Exempt limits"; the coordinator's pick under the owner's delegation, open to the owner's veto.
53const LONGEST_REASON: usize = 4000;
54
55/// First line of a record that holds its parts as fields; a file without it is the finished text an earlier release wrote.
56const RECORD_MAGIC: &str = "cli-git push notice 1";
57
58/// First line of a record of a push the service did not make because the branch has no destination it can name; its fields are
59/// those of a failure record, and its reason is the service's own words.
60const SKIPPED_MAGIC: &str = "cli-git push skipped 1";
61
62/// The field names of a record's header lines.
63const BRANCH_FIELD: &str = "branch: ";
64const COMMIT_FIELD: &str = "commit: ";
65
66/// Said when Git gave no words at all.
67const NO_REASON: &str = "Git gave no reason.";
68
69/// The directory that holds the records of a repository.
70pub fn notice_directory(git_dir: &Path) -> PathBuf {
71 return git_dir.join(NOTICE_DIRECTORY);
72}
73
74/// A 64-bit FNV-1a hash of the text, in hexadecimal.
75pub fn fnv_hex(text: &str) -> String {
76 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
77 for byte in text.bytes() {
78 hash ^= u64::from(byte);
79 hash = hash.wrapping_mul(0x0100_0000_01b3);
80 }
81 return format!("{hash:016x}");
82}
83
84/// The part of every record name that belongs to one branch.
85fn branch_prefix(branch_ref: &str) -> String {
86 let key: String = encode_branch_key(branch_ref);
87 let shown: String = if key.len() > LONGEST_KEY { format!("{}~{}", &key[..LONGEST_KEY], fnv_hex(key.as_str())) } else { key };
88 return format!("push{NAME_SEPARATOR}{shown}{NAME_SEPARATOR}");
89}
90
91/// The file name of the record of one failed push.
92pub fn record_name(branch_ref: &str, commit: &str) -> String {
93 return format!("{}{commit}.txt", branch_prefix(branch_ref));
94}
95
96/// What:
97/// Git's words for the person: the user name and password of every URL masked, control characters dropped, and the
98/// length bounded.
99/// Why:
100/// The text came from a remote and from Git, and is printed on a terminal; a URL with a user name and password, or an
101/// escape sequence, must not reach the screen or a log of it. Only the credentials are masked, never the line around them,
102/// because the line often says why the push failed (the wrapper's own Git LFS failure line names the endpoint setting and
103/// carries the exit status).
104///
105/// In TS you'd write (pseudocode):
106/// ```ts
107/// cleanReason(said)
108/// ```
109pub fn clean_reason(said: &str) -> String {
110 let masked: String = mask_userinfo_text(said);
111 let mut kept: Vec<String> = Vec::new();
112 for line in masked.lines() {
113 let plain: String = line.chars().filter(|character| { return *character == '\t' || !character.is_control(); }).collect();
114 let trimmed: &str = plain.trim_end();
115 if !trimmed.is_empty() {
116 kept.push(trimmed.to_string());
117 }
118 }
119 let mut text: String = kept.join("\n");
120 if text.len() > LONGEST_REASON {
121 let mut cut: usize = LONGEST_REASON;
122 while !text.is_char_boundary(cut) {
123 cut -= 1;
124 }
125 text.truncate(cut);
126 text.push_str("\n(Git's output is cut here.)");
127 }
128 return text;
129}
130
131/// What:
132/// The text shown for a failed backup push, with the commit named as given and Git's words already cleaned.
133/// Why:
134/// It names the branch and the commit, gives Git's own reason, says the commit is safe on this computer, and says what to
135/// do; the service runs with its own settings, which is the first thing to check when a push that works in a terminal
136/// fails here. The command that shows it may be anyone's, so it never calls the commit the reader's own.
137///
138/// In TS you'd write (pseudocode):
139/// ```ts
140/// worded({ branchRef, shown, reason })
141/// ```
142fn worded(branch_ref: &str, shown: &str, reason: &str) -> String {
143 let branch: &str = branch_ref.strip_prefix("refs/heads/").unwrap_or(branch_ref);
144 let detail: String = if reason.is_empty() {
145 NO_REASON.to_string()
146 } else {
147 format!("Git said:\n{}", reason.lines().map(|line| { return format!(" {line}"); }).collect::<Vec<String>>().join("\n"))
148 };
149 return format!(
150 "The backup push of branch {branch} failed for commit {shown}, so that commit exists only on this computer for now. \
151 It is saved in this repository and nothing was lost.\n{detail}\n\
152 Fix what Git names, then run `git push`. If it says the remote has commits you do not have, run `git pull` first. \
153 The commit service runs with its own settings, not your terminal's, so a push that works in your terminal can \
154 fail here when it needs a variable only your terminal has; give the service such a variable with \
155 `systemctl --user set-environment NAME=value` and then `{RESTART_COMMAND}`.\n{PUSH_FAILED_NOTE}"
156 );
157}
158
159/// What:
160/// The text shown for a failed backup push, from Git's raw words.
161/// Why:
162/// The commit is named as the caller gives it, so a caller that has the repository passes Git's own abbreviation.
163///
164/// In TS you'd write (pseudocode):
165/// ```ts
166/// pushFailureText({ branchRef, commit, said })
167/// ```
168pub fn push_failure_text(branch_ref: &str, commit: &str, said: &str) -> String {
169 return worded(branch_ref, commit, clean_reason(said).as_str());
170}
171
172/// What:
173/// The text shown for a push the service did not make, with the commit named as given and the service's words already cleaned.
174/// Why:
175/// Nothing failed and nothing was sent, so the text says that and not that the push was refused; it names the branch and the
176/// commit, gives the service's reason, says the commit is saved here, and names both ways out: a destination the service can
177/// name, or a push by hand. The command that shows it may be anyone's, so it never calls the commit the reader's own.
178///
179/// In TS you'd write (pseudocode):
180/// ```ts
181/// wordedSkipped({ branchRef, shown, reason })
182/// ```
183fn worded_skipped(branch_ref: &str, shown: &str, reason: &str) -> String {
184 let branch: &str = branch_ref.strip_prefix("refs/heads/").unwrap_or(branch_ref);
185 let detail: String = if reason.is_empty() {
186 String::from("The commit service gave no reason.")
187 } else {
188 format!("The commit service said:\n{}", reason.lines().map(|line| { return format!(" {line}"); }).collect::<Vec<String>>().join("\n"))
189 };
190 return format!(
191 "The backup push of branch {branch} for commit {shown} was not made, so that commit exists only on this computer for now. \
192 It is saved in this repository and nothing was lost.\n{detail}\n\
193 Either give the branch a destination the commit service can name (the `push.default` setting of `git help config` says \
194 which values do), or push it yourself with `git push <remote> {branch}`."
195 );
196}
197
198/// What:
199/// Leaves the record of one push the service did not make, replacing an earlier record of the same commit on the same branch.
200/// Why:
201/// The service settles such a push as done because nothing is left for it to retry, and the person would otherwise never
202/// learn that the backup does not exist. The name is the failure record's, so a later successful push of the branch removes
203/// it with the failures.
204///
205/// In TS you'd write (pseudocode):
206/// ```ts
207/// await recordPushSkip(gitDir, { branchRef, commit, said })
208/// ```
209pub fn record_push_skip(git_dir: &Path, branch_ref: &str, commit: &str, said: &str) -> Result<(), String> {
210 let directory: PathBuf = notice_directory(git_dir);
211 create_private_directory_all(directory.as_path()).map_err(|error| { return format!("creating the notice directory failed: {error}"); })?;
212 let path: PathBuf = directory.join(record_name(branch_ref, commit));
213 let _removed = std::fs::remove_file(path.as_path());
214 let text: String = format!("{SKIPPED_MAGIC}\n{BRANCH_FIELD}{branch_ref}\n{COMMIT_FIELD}{commit}\n\n{}", clean_reason(said));
215 return write_private_file_unsynced(path.as_path(), text.as_bytes()).map_err(|error| { return format!("writing the notice failed: {error}"); });
216}
217
218/// What:
219/// Leaves the record of one failed push, replacing an earlier record of the same commit on the same branch.
220/// Why:
221/// The name is derived from the branch and the commit, so a push that is tried again and fails again leaves one record.
222/// The reason is cleaned before it is written, so no credential reaches the disk.
223///
224/// In TS you'd write (pseudocode):
225/// ```ts
226/// await recordPushFailure(gitDir, { branchRef, commit, said })
227/// ```
228pub fn record_push_failure(git_dir: &Path, branch_ref: &str, commit: &str, said: &str) -> Result<(), String> {
229 let directory: PathBuf = notice_directory(git_dir);
230 create_private_directory_all(directory.as_path()).map_err(|error| { return format!("creating the notice directory failed: {error}"); })?;
231 let path: PathBuf = directory.join(record_name(branch_ref, commit));
232 // A repeat of the same failure replaces its record.
233 let _removed = std::fs::remove_file(path.as_path());
234 let text: String = format!("{RECORD_MAGIC}\n{BRANCH_FIELD}{branch_ref}\n{COMMIT_FIELD}{commit}\n\n{}", clean_reason(said));
235 return write_private_file_unsynced(path.as_path(), text.as_bytes()).map_err(|error| { return format!("writing the notice failed: {error}"); });
236}
237
238/// What:
239/// The text shown for a push that had not finished when the worktree that made the commit was removed.
240/// Why:
241/// Nothing says whether the push ran, failed or never started, so the text says only what is known: the commit is saved here
242/// and the branch may not be on the remote.
243fn worded_lost(branch_ref: &str, shown: &str) -> String {
244 let branch: &str = branch_ref.strip_prefix("refs/heads/").unwrap_or(branch_ref);
245 return format!(
246 "The backup push of branch {branch} for commit {shown} had not finished when the worktree that made the commit was \
247 removed, so that commit may exist only on this computer for now. It is saved in this repository and nothing was lost. \
248 Run `git push` in a worktree that has branch {branch} checked out, or `git push <remote> {branch}` from any worktree."
249 );
250}
251
252/// What:
253/// Leaves the record of a push that was lost with its worktree, unless a record of that commit on that branch is already
254/// there.
255/// Why:
256/// An existing record holds Git's own words about a failed push, which say more than this one does.
257///
258/// In TS you'd write (pseudocode):
259/// ```ts
260/// await recordLostPush(commonDir, { branchRef, commit })
261/// ```
262pub fn record_lost_push(common_dir: &Path, branch_ref: &str, commit: &str) -> Result<(), String> {
263 let directory: PathBuf = common_dir.join(ORPHAN_NOTICE_DIRECTORY);
264 create_private_directory_all(directory.as_path()).map_err(|error| { return format!("creating the notice directory failed: {error}"); })?;
265 let path: PathBuf = directory.join(record_name(branch_ref, commit));
266 if path.exists() {
267 return Ok(());
268 }
269 let text: String = format!("{LOST_MAGIC}\n{BRANCH_FIELD}{branch_ref}\n{COMMIT_FIELD}{commit}\n\n");
270 return match write_private_file_unsynced(path.as_path(), text.as_bytes()) {
271 Err(error) if error.kind() != std::io::ErrorKind::AlreadyExists => Err(format!("writing the notice failed: {error}")),
272 Ok(()) | Err(_) => Ok(()),
273 };
274}
275
276/// What a record says besides its branch and commit.
277enum RecordKind<'a> {
278 /// A failed push, with Git's cleaned words.
279 Failed(&'a str),
280 /// A push the service did not make, with its cleaned words.
281 Skipped(&'a str),
282 /// A push lost with its worktree.
283 Lost,
284}
285
286/// The branch, the commit and the kind of a record written by this release.
287fn parse_record(text: &str) -> Option<(&str, &str, RecordKind<'_>)> {
288 let mut lines = text.splitn(5, '\n');
289 let magic: &str = lines.next()?;
290 if ![RECORD_MAGIC, SKIPPED_MAGIC, LOST_MAGIC].contains(&magic) {
291 return None;
292 }
293 let branch: &str = lines.next()?.strip_prefix(BRANCH_FIELD)?;
294 let commit: &str = lines.next()?.strip_prefix(COMMIT_FIELD)?;
295 if !lines.next()?.is_empty() {
296 return None;
297 }
298 let reason: &str = lines.next().unwrap_or("");
299 return Some((branch, commit, match magic {
300 LOST_MAGIC => RecordKind::Lost,
301 SKIPPED_MAGIC => RecordKind::Skipped(reason),
302 _ => RecordKind::Failed(reason),
303 }));
304}
305
306/// What:
307/// Removes the records of one branch, after a push of that branch succeeded.
308/// Why:
309/// A successful push of the branch carries every earlier commit of it, so an earlier failure is no longer true.
310///
311/// In TS you'd write (pseudocode):
312/// ```ts
313/// await clearPushFailures(gitDir, branchRef)
314/// ```
315pub fn clear_push_failures(git_dir: &Path, branch_ref: &str) {
316 clear_push_failures_before(git_dir, branch_ref, std::time::SystemTime::now());
317}
318
319/// What:
320/// Removes the records of one branch that were written no later than `cutoff`, after a push of that branch succeeded.
321/// Why:
322/// A push carries the commits that existed when it began. A record written after that moment is about a newer commit, which
323/// another instance of the service may have pushed and failed to push while this push's result was still being handled; the
324/// older push says nothing about it, so it stays.
325///
326/// In TS you'd write (pseudocode):
327/// ```ts
328/// await clearPushFailuresBefore(gitDir, branchRef, pushBegan)
329/// ```
330pub fn clear_push_failures_before(git_dir: &Path, branch_ref: &str, cutoff: std::time::SystemTime) {
331 let Ok(listing) = std::fs::read_dir(notice_directory(git_dir)) else { return };
332 let prefix: String = branch_prefix(branch_ref);
333 for entry in listing.flatten() {
334 let older: bool = entry.metadata().and_then(|meta| { return meta.modified(); }).is_ok_and(|written| { return written <= cutoff; });
335 if older && entry.file_name().to_string_lossy().starts_with(prefix.as_str()) {
336 let _removed = std::fs::remove_file(entry.path());
337 }
338 }
339}
340
341/// Where the worktrees of a repository stand: the branch the asking worktree has checked out and every branch any
342/// worktree has checked out.
343#[derive(Clone, Debug, Default, Eq, PartialEq)]
344pub struct Worktrees {
345 /// The branch reference the command's worktree has checked out; nothing for a detached head or an unknown worktree.
346 pub here: Option<String>,
347 /// The branch references checked out in every worktree of the repository.
348 pub all: Vec<String>,
349}
350
351/// What:
352/// The branch reference a `HEAD` file names, or nothing for a detached head or an unreadable file.
353/// Why:
354/// Only the plain `ref: refs/heads/<name>` form names a branch.
355///
356/// In TS you'd write (pseudocode):
357/// ```ts
358/// branchOfHead(gitDir)
359/// ```
360pub fn branch_of_head(git_dir: &Path) -> Option<String> {
361 let text: String = std::fs::read_to_string(git_dir.join("HEAD")).ok()?;
362 let target: &str = text.lines().next()?.strip_prefix("ref: ")?.trim();
363 return target.starts_with("refs/heads/").then(|| { return target.to_string(); });
364}
365
366/// What:
367/// The branches checked out in the repository's worktrees: the main one and each linked one.
368/// Why:
369/// A linked worktree keeps its own `HEAD` under `worktrees/<name>`; the main worktree's is the common directory's.
370///
371/// In TS you'd write (pseudocode):
372/// ```ts
373/// checkedOutBranches(commonDir)
374/// ```
375pub fn checked_out_branches(common_dir: &Path) -> Vec<String> {
376 let mut branches: Vec<String> = branch_of_head(common_dir).into_iter().collect();
377 if let Ok(listing) = std::fs::read_dir(common_dir.join("worktrees")) {
378 for entry in listing.flatten() {
379 branches.extend(branch_of_head(entry.path().as_path()));
380 }
381 }
382 return branches;
383}
384
385/// Whether a record of `branch_ref` is for the asking worktree: it has the branch checked out, or no worktree has.
386fn is_for(worktrees: &Worktrees, branch_ref: &str) -> bool {
387 return worktrees.here.as_deref() == Some(branch_ref) || !worktrees.all.iter().any(|held| { return held == branch_ref; });
388}
389
390/// What:
391/// Reads and removes the records that are for the asking worktree (all of them when no worktree is given), oldest name
392/// first, worded with the commit named by `abbreviate`.
393/// Why:
394/// A record is shown once, to the worktree whose commit it is about; another worktree's command leaves it where it is.
395/// When nothing failed the notice directory does not exist, and this is one directory read.
396///
397/// In TS you'd write (pseudocode):
398/// ```ts
399/// const texts = await takePushFailuresFor(gitDir, worktrees, abbreviate)
400/// ```
401pub fn take_push_failures_for(git_dir: &Path, worktrees: Option<&Worktrees>, abbreviate: &dyn Fn(&str) -> String) -> Vec<String> {
402 return take_records_from(notice_directory(git_dir).as_path(), worktrees, abbreviate);
403}
404
405/// What:
406/// Reads and removes the records of removed worktrees, which belong to no worktree and so are shown to the first command
407/// that asks.
408/// Why:
409/// The worktree a record was made for is gone, so nobody else could ever be shown it; one directory read when none exist.
410///
411/// In TS you'd write (pseudocode):
412/// ```ts
413/// const texts = await takeOrphanedFailures(commonDir, abbreviate)
414/// ```
415pub fn take_orphaned_failures(common_dir: &Path, abbreviate: &dyn Fn(&str) -> String) -> Vec<String> {
416 return take_records_from(common_dir.join(ORPHAN_NOTICE_DIRECTORY).as_path(), None, abbreviate);
417}
418
419/// Reads and removes the records in one directory that are for the asking worktree, oldest name first.
420fn take_records_from(directory: &Path, worktrees: Option<&Worktrees>, abbreviate: &dyn Fn(&str) -> String) -> Vec<String> {
421 let Ok(listing) = std::fs::read_dir(directory) else { return Vec::new() };
422 let mut paths: Vec<PathBuf> = listing.flatten().map(|entry| { return entry.path(); }).collect();
423 paths.sort();
424 let mut texts: Vec<String> = Vec::new();
425 for path in paths {
426 let Ok(text) = std::fs::read_to_string(path.as_path()) else { continue };
427 let parsed: Option<(&str, &str, RecordKind<'_>)> = parse_record(text.as_str());
428 if let (Some((branch, _, _)), Some(asking)) = (&parsed, worktrees)
429 && !is_for(asking, branch)
430 {
431 continue;
432 }
433 // The record is removed before its text is used, so two commands that start together cannot both show it.
434 if std::fs::remove_file(path.as_path()).is_err() {
435 continue;
436 }
437 texts.push(match parsed {
438 Some((branch, commit, RecordKind::Failed(reason))) => worded(branch, abbreviate(commit).as_str(), reason),
439 Some((branch, commit, RecordKind::Skipped(reason))) => worded_skipped(branch, abbreviate(commit).as_str(), reason),
440 Some((branch, commit, RecordKind::Lost)) => worded_lost(branch, abbreviate(commit).as_str()),
441 // The finished text an earlier release wrote.
442 None => text,
443 });
444 }
445 return texts;
446}
447
448/// What:
449/// Reads and removes every record, with each commit named in full.
450/// Why:
451/// The service's own controls and a repository with no worktree to ask need all of them.
452///
453/// In TS you'd write (pseudocode):
454/// ```ts
455/// const texts = await takePushFailures(gitDir)
456/// ```
457pub fn take_push_failures(git_dir: &Path) -> Vec<String> {
458 return take_push_failures_for(git_dir, None, &|commit| { return commit.to_string(); });
459}
460
461/// Commands that a program reads the output of, or that carry a protocol on their streams.
462const MACHINE_COMMANDS: &[&[u8]] = &[
463 b"cat-file", b"check-attr", b"check-ignore", b"check-ref-format", b"commit-tree", b"config", b"credential", b"diff-files",
464 b"diff-index", b"diff-tree", b"fast-export", b"fast-import", b"for-each-ref", b"hash-object", b"index-pack", b"ls-files",
465 b"ls-tree", b"merge-base", b"mktree", b"name-rev", b"pack-objects", b"read-tree", b"receive-pack", b"rev-list",
466 b"rev-parse", b"show-ref", b"symbolic-ref", b"unpack-objects", b"update-ref", b"upload-archive", b"upload-pack",
467 b"var", b"write-tree", b"update-index", b"verify-pack",
468];
469
470/// Option spellings that ask a command for output made to be read by a program.
471fn asks_for_machine_output(option: &[u8]) -> bool {
472 return option == b"-z"
473 || option == b"--null"
474 || option.starts_with(b"--porcelain")
475 || option.starts_with(b"--format")
476 || option.starts_with(b"--pretty");
477}
478
479/// What:
480/// Whether a command's output is read by a program, so that no record may be shown to it or taken from it.
481/// Why:
482/// A prompt, an editor or a script runs `git status --porcelain` or `git rev-parse` often and reads every byte; the
483/// record is meant for the person, and the first such call would otherwise take it.
484///
485/// In TS you'd write (pseudocode):
486/// ```ts
487/// isMachineRead(commandWord, region)
488/// ```
489pub fn is_machine_read(command: &[u8], region: &[OsString]) -> bool {
490 if MACHINE_COMMANDS.contains(&command) {
491 return true;
492 }
493 for argument in region {
494 let bytes: &[u8] = argument.as_encoded_bytes();
495 // Everything after the separator is a name, not an option.
496 if bytes == b"--" {
497 break;
498 }
499 if asks_for_machine_output(bytes) {
500 return true;
501 }
502 }
503 return false;
504}
505
506/// Record controls stay out of the release executable.
507#[cfg(test)]
508#[path = "push_failure_notice_tests.rs"]
509mod tests;