just playing with tangled
at ig/vimdiffwarn 209 lines 6.0 kB view raw
1// Copyright 2020 The Jujutsu Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15use std::fmt; 16use std::fmt::Debug; 17use std::fmt::Display; 18use std::process::ExitStatus; 19 20/// Command output and exit status to be displayed in normalized form. 21#[derive(Clone, Debug, Eq, PartialEq)] 22pub struct CommandOutput { 23 pub stdout: CommandOutputString, 24 pub stderr: CommandOutputString, 25 pub status: ExitStatus, 26} 27 28impl CommandOutput { 29 /// Normalizes Windows directory separator to slash. 30 #[must_use] 31 pub fn normalize_backslash(self) -> Self { 32 CommandOutput { 33 stdout: self.stdout.normalize_backslash(), 34 stderr: self.stderr.normalize_backslash(), 35 status: self.status, 36 } 37 } 38 39 /// Normalizes [`ExitStatus`] message in stderr text. 40 #[must_use] 41 pub fn normalize_stderr_exit_status(self) -> Self { 42 CommandOutput { 43 stdout: self.stdout, 44 stderr: self.stderr.normalize_exit_status(), 45 status: self.status, 46 } 47 } 48 49 /// Removes the last line (such as platform-specific error message) from the 50 /// normalized stderr text. 51 #[must_use] 52 pub fn strip_stderr_last_line(self) -> Self { 53 CommandOutput { 54 stdout: self.stdout, 55 stderr: self.stderr.strip_last_line(), 56 status: self.status, 57 } 58 } 59 60 #[must_use] 61 pub fn normalize_stdout_with(self, f: impl FnOnce(String) -> String) -> Self { 62 CommandOutput { 63 stdout: self.stdout.normalize_with(f), 64 stderr: self.stderr, 65 status: self.status, 66 } 67 } 68 69 #[must_use] 70 pub fn normalize_stderr_with(self, f: impl FnOnce(String) -> String) -> Self { 71 CommandOutput { 72 stdout: self.stdout, 73 stderr: self.stderr.normalize_with(f), 74 status: self.status, 75 } 76 } 77 78 /// Ensures that the command exits with success status. 79 #[track_caller] 80 pub fn success(self) -> Self { 81 assert!(self.status.success(), "{self}"); 82 self 83 } 84} 85 86impl Display for CommandOutput { 87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 88 let CommandOutput { 89 stdout, 90 stderr, 91 status, 92 } = self; 93 write!(f, "{stdout}")?; 94 if !stderr.is_empty() { 95 writeln!(f, "------- stderr -------")?; 96 write!(f, "{stderr}")?; 97 } 98 if !status.success() { 99 // If there is an exit code, `{status}` would get rendered as "exit 100 // code: N" on Windows, so we render it ourselves for compatibility. 101 if let Some(code) = status.code() { 102 writeln!(f, "[exit status: {code}]")?; 103 } else { 104 writeln!(f, "[{status}]")?; 105 } 106 } 107 Ok(()) 108 } 109} 110 111/// Command output data to be displayed in normalized form. 112#[derive(Clone)] 113pub struct CommandOutputString { 114 // TODO: use BString? 115 pub(super) raw: String, 116 pub(super) normalized: String, 117} 118 119impl CommandOutputString { 120 /// Normalizes Windows directory separator to slash. 121 #[must_use] 122 pub fn normalize_backslash(self) -> Self { 123 self.normalize_with(|s| s.replace('\\', "/")) 124 } 125 126 /// Normalizes [`ExitStatus`] message. 127 /// 128 /// On Windows, it prints "exit code" instead of "exit status". 129 #[must_use] 130 pub fn normalize_exit_status(self) -> Self { 131 self.normalize_with(|s| s.replace("exit code:", "exit status:")) 132 } 133 134 /// Removes the last line (such as platform-specific error message) from the 135 /// normalized text. 136 #[must_use] 137 pub fn strip_last_line(self) -> Self { 138 self.normalize_with(|mut s| { 139 s.truncate(strip_last_line(&s).len()); 140 s 141 }) 142 } 143 144 #[must_use] 145 pub fn normalize_with(mut self, f: impl FnOnce(String) -> String) -> Self { 146 self.normalized = f(self.normalized); 147 self 148 } 149 150 #[must_use] 151 pub fn is_empty(&self) -> bool { 152 self.raw.is_empty() 153 } 154 155 /// Raw output data. 156 #[must_use] 157 pub fn raw(&self) -> &str { 158 &self.raw 159 } 160 161 /// Normalized text for snapshot testing. 162 #[must_use] 163 pub fn normalized(&self) -> &str { 164 &self.normalized 165 } 166 167 /// Extracts raw output data. 168 #[must_use] 169 pub fn into_raw(self) -> String { 170 self.raw 171 } 172} 173 174impl Debug for CommandOutputString { 175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 176 // Print only raw data. Normalized string should be nearly identical. 177 Debug::fmt(&self.raw, f) 178 } 179} 180 181impl Display for CommandOutputString { 182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 183 if self.is_empty() { 184 return Ok(()); 185 } 186 // Append "[EOF]" marker to test line ending 187 // https://github.com/mitsuhiko/insta/issues/384 188 writeln!(f, "{}[EOF]", self.normalized) 189 } 190} 191 192impl Eq for CommandOutputString {} 193 194impl PartialEq for CommandOutputString { 195 fn eq(&self, other: &Self) -> bool { 196 // Compare only raw data. Normalized string is for displaying purpose. 197 self.raw == other.raw 198 } 199} 200 201/// Returns a string with the last line removed. 202/// 203/// Use this to remove the root error message containing platform-specific 204/// content for example. 205pub fn strip_last_line(s: &str) -> &str { 206 s.trim_end_matches('\n') 207 .rsplit_once('\n') 208 .map_or(s, |(h, _)| &s[..h.len() + 1]) 209}