just playing with tangled
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}