1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
//! What:
//! Test-only fault points of the resident service: `CLI_GIT_TEST_ONLY_SERVICE_FAULT=<point>` ends the service with
//! `SIGKILL` the moment it reaches the named point of a batch.
//! Why:
//! The kill tests need a deterministic instant, as the transaction phase markers give the wrapper
//! (`transaction_phase.rs`). The marker is compiled into the release executable, and only the explicit variable arms it.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! if (process.env.CLI_GIT_TEST_ONLY_SERVICE_FAULT === point) process.kill(process.pid, 'SIGKILL');
//! ```
/// Environment variable arming one fault point.
pub const FAULT_VARIABLE: &str = "CLI_GIT_TEST_ONLY_SERVICE_FAULT";
/// The instants of a batch at which a kill is tested, in the order a batch reaches them.
pub const FAULT_POINTS: [&str; 5] = ["after-objects", "after-intent", "after-ref", "after-index", "before-ack"];
/// What:
/// Ends the process if `point` is the armed one.
/// Why:
/// A kill at a point is the strongest crash a test can name without injecting a system call failure.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// reach('after-intent')
/// ```
pub fn reach(point: &str) {
#[cfg(test)]
hooks::run(point);
if std::env::var(FAULT_VARIABLE).is_ok_and(|armed| { return armed == point; }) {
crate::service_log!("test fault at {point}");
#[cfg(unix)]
// SAFETY: `kill` takes no pointers; the process ends itself.
unsafe {
libc::kill(libc::getpid(), libc::SIGKILL);
}
std::process::exit(137);
}
}
/// What:
/// Says that a step of a landing happened, for a control that checks the order of durable steps.
/// Why:
/// A directory sync cannot be seen from outside the process, so a control reads the order the landing reports; the release
/// executable keeps nothing.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// note('sync-index-dir')
/// ```
pub fn note(event: &str) {
#[cfg(test)]
hooks::record(event);
#[cfg(not(test))]
let _unused: &str = event;
}
/// What:
/// Whether a control made the step at `point` fail.
/// Why:
/// A landing whose index update fails after the branch moved is the case recovery exists for, and Git cannot be made to fail
/// there on demand. A control names the point; the release executable never fails a step this way.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// if (fails('build-index')) return false
/// ```
pub fn fails(point: &str) -> bool {
#[cfg(test)]
return hooks::failing(point);
#[cfg(not(test))]
return {
let _unused: &str = point;
false
};
}
/// Environment variable naming a directory in which the service leaves one file for each commit request it has queued.
pub const QUEUED_DIRECTORY_VARIABLE: &str = "CLI_GIT_TEST_ONLY_SERVICE_QUEUED_DIRECTORY";
/// What:
/// Leaves a file named for request `id` in the directory the variable names, when it is armed.
/// Why:
/// A control that sends requests in a set order needs to know that the service holds one before it sends the next; a pause
/// of its own can only guess. The file appears after the request is in the queue, so what the control sees is the service's own
/// state. Only the explicit variable arms it, as with the fault points.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// if (process.env.CLI_GIT_TEST_ONLY_SERVICE_QUEUED_DIRECTORY) writeFile(join(dir, safe(id)), '')
/// ```
pub fn queued(id: &str) {
let Some(directory) = std::env::var_os(QUEUED_DIRECTORY_VARIABLE) else { return };
let name: String = id.chars().map(|character| { return if character.is_ascii_alphanumeric() { character } else { '_' }; }).collect();
let path: std::path::PathBuf = std::path::Path::new(&directory).join(format!("queued-{name}"));
if let Err(error) = std::fs::write(&path, b"") {
crate::service_warn!("cannot leave the queued-request file {}: {error}", path.display());
}
}
/// Environment variable pinning the number the service counts its batches from, in place of the clock reading it starts from.
pub const FIRST_BATCH_VARIABLE: &str = "CLI_GIT_TEST_ONLY_SERVICE_FIRST_BATCH";
/// What:
/// The number the variable pins the service's batch count to, when it is armed with a number.
/// Why:
/// Two services that share a journal count from clock readings, which are equal only by chance; a control that needs them equal
/// pins both. Only the explicit variable arms it, as with the fault points.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const pinned = Number(process.env.CLI_GIT_TEST_ONLY_SERVICE_FIRST_BATCH) // undefined when unset or not a number
/// ```
pub fn pinned_first_batch() -> Option<u64> {
return std::env::var(FIRST_BATCH_VARIABLE).ok().and_then(|text| { return text.parse::<u64>().ok(); });
}
/// Hooks a control sets to run its own code at a point of a landing, on the thread that reaches the point; never part of the
/// release executable.
#[cfg(test)]
pub(crate) mod hooks {
use std::cell::RefCell;
thread_local! {
/// The steps of a landing on this thread, in the order they happened.
static EVENTS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
/// The point and the code to run at it.
static HOOK: RefCell<Option<(String, std::rc::Rc<dyn Fn()>)>> = const { RefCell::new(None) };
/// The points whose steps fail on this thread.
static FAILING: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
/// Make the step at `point` fail on this thread's landings until `clear_failures`.
pub(crate) fn fail_at(point: &str) {
FAILING.with(|failing| { failing.borrow_mut().push(point.to_string()); });
}
/// Let every step succeed again.
pub(crate) fn clear_failures() {
FAILING.with(|failing| { failing.borrow_mut().clear(); });
}
/// Whether the step at `point` fails.
pub(super) fn failing(point: &str) -> bool {
return FAILING.with(|failing| { return failing.borrow().iter().any(|named| { return named == point; }); });
}
/// Run `code` whenever the landing of this thread reaches `point`.
pub(crate) fn set(point: &str, code: std::rc::Rc<dyn Fn()>) {
HOOK.with(|hook| { *hook.borrow_mut() = Some((point.to_string(), code)); });
}
/// Remember that `event` happened.
pub(super) fn record(event: &str) {
EVENTS.with(|events| { events.borrow_mut().push(event.to_string()); });
}
/// The steps reported on this thread since the last call, in order.
pub(crate) fn take_events() -> Vec<String> {
return EVENTS.with(|events| { return std::mem::take(&mut *events.borrow_mut()); });
}
/// Run the hook if it is for `point`.
pub(super) fn run(point: &str) {
let found: Option<std::rc::Rc<dyn Fn()>> = HOOK.with(|hook| { return hook.borrow().as_ref().filter(|(named, _)| { return named == point; }).map(|(_, code)| { return std::rc::Rc::clone(code); }); });
if let Some(hook) = found {
hook();
}
}
}