actions_core/
core.rs

1use std::env;
2use std::io::{self, Write};
3
4use crate::logger::{Log, LogLevel};
5use crate::util;
6
7use uuid::Uuid;
8
9const PATH_VAR: &str = "PATH";
10
11#[cfg(not(windows))]
12pub(crate) const DELIMITER: &str = ":";
13
14#[cfg(windows)]
15pub(crate) const DELIMITER: &str = ";";
16
17pub struct Core<W> {
18	out: W,
19}
20
21impl Default for Core<std::io::Stdout> {
22	fn default() -> Self {
23		Self {
24			out: std::io::stdout(),
25		}
26	}
27}
28
29impl Core<std::io::Stdout> {
30	pub fn new() -> Self {
31		Default::default()
32	}
33}
34
35impl<W: Write> From<W> for Core<W> {
36	fn from(out: W) -> Self {
37		Core { out }
38	}
39}
40
41impl<W> Core<W>
42where
43	W: Write,
44{
45	fn issue<V: ToString>(&mut self, k: &str, v: V) -> io::Result<()> {
46		writeln!(self.out, "::{}::{}", k, util::escape_data(v))
47	}
48
49	fn issue_named<K: ToString, V: ToString>(
50		&mut self,
51		name: &str,
52		k: K,
53		v: V,
54	) -> io::Result<()> {
55		writeln!(
56			self.out,
57			"::{} {}::{}",
58			name,
59			util::cmd_arg("name", k),
60			util::escape_data(v),
61		)
62	}
63
64	pub fn input<K: ToString>(
65		_: &Self,
66		name: K,
67	) -> Result<String, env::VarError> {
68		crate::input(name)
69	}
70
71	pub fn set_output<K: ToString, V: ToString>(
72		&mut self,
73		k: K,
74		v: V,
75	) -> io::Result<()> {
76		self.issue_named("set-output", k, v.to_string())
77	}
78
79	pub fn set_env<K: ToString, V: ToString>(
80		&mut self,
81		k: K,
82		v: V,
83	) -> io::Result<()> {
84		let v = v.to_string();
85
86		// TODO: Move the side effect to a struct member
87		env::set_var(k.to_string(), &v);
88
89		self.issue_named("set-env", k, v)
90	}
91
92	pub fn add_mask<V: ToString>(&mut self, v: V) -> io::Result<()> {
93		self.issue("add-mask", v)
94	}
95
96	pub fn add_path<P: ToString>(&mut self, v: P) -> io::Result<()> {
97		let v = v.to_string();
98
99		self.issue("add-path", &v)?;
100
101		// TODO: Move the side effect to a struct member
102		let path = if let Some(mut path) = env::var_os(PATH_VAR) {
103			path.push(DELIMITER);
104			path.push(v);
105
106			path
107		} else {
108			v.into()
109		};
110
111		env::set_var(PATH_VAR, path);
112
113		Ok(())
114	}
115
116	pub fn save_state<K: ToString, V: ToString>(
117		&mut self,
118		k: K,
119		v: V,
120	) -> io::Result<()> {
121		self.issue_named("save-state", k, v.to_string())
122	}
123
124	pub fn state<K: ToString>(
125		_: &Self,
126		name: K,
127	) -> Result<String, env::VarError> {
128		crate::state(name)
129	}
130
131	// TODO: Should the API prevent compiling code that will output commands
132	// while this is running?
133	pub fn stop_logging<F, T>(&mut self, f: F) -> io::Result<T>
134	where
135		F: FnOnce() -> T,
136	{
137		// TODO: Allow the to be configurable (helpful for tests)
138		let token = Uuid::new_v4().to_string();
139
140		self.issue("stop-commands", &token)?;
141
142		let result = f();
143
144		self.issue(&token, "")?;
145
146		Ok(result)
147	}
148
149	pub fn is_debug(_: &Self) -> bool {
150		crate::is_debug()
151	}
152
153	pub fn log_message<M: ToString>(
154		&mut self,
155		level: LogLevel,
156		message: M,
157	) -> io::Result<()> {
158		self.issue(level.as_ref(), message)
159	}
160
161	pub fn debug<M: ToString>(&mut self, message: M) -> io::Result<()> {
162		self.log_message(LogLevel::Debug, message)
163	}
164
165	pub fn error<M: ToString>(&mut self, message: M) -> io::Result<()> {
166		self.log_message(LogLevel::Error, message)
167	}
168
169	pub fn warning<M: ToString>(&mut self, message: M) -> io::Result<()> {
170		self.log_message(LogLevel::Warning, message)
171	}
172
173	pub fn log<M: ToString>(
174		&mut self,
175		level: LogLevel,
176		log: Log<M>,
177	) -> io::Result<()> {
178		writeln!(self.out, "::{}{}", level.as_ref(), log)
179	}
180
181	pub fn log_debug<M: ToString>(&mut self, log: Log<M>) -> io::Result<()> {
182		self.log(LogLevel::Debug, log)
183	}
184
185	pub fn log_error<M: ToString>(&mut self, log: Log<M>) -> io::Result<()> {
186		self.log(LogLevel::Error, log)
187	}
188
189	pub fn log_warning<M: ToString>(&mut self, log: Log<M>) -> io::Result<()> {
190		self.log(LogLevel::Warning, log)
191	}
192}
193
194#[cfg(test)]
195mod test {
196	use std::cell::RefCell;
197	use std::env;
198	use std::io;
199	use std::rc::Rc;
200
201	use crate::core::DELIMITER;
202	use crate::*;
203
204	#[derive(Clone)]
205	struct TestBuf {
206		inner: Rc<RefCell<Vec<u8>>>,
207	}
208
209	impl TestBuf {
210		fn new() -> Self {
211			Self {
212				inner: Rc::new(RefCell::new(Vec::new())),
213			}
214		}
215
216		fn clear(&self) {
217			self.inner.borrow_mut().clear();
218		}
219
220		fn to_string(&self) -> String {
221			String::from_utf8(self.inner.borrow().to_vec()).unwrap()
222		}
223	}
224
225	impl io::Write for TestBuf {
226		fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
227			self.inner.borrow_mut().write(buf)
228		}
229
230		fn flush(&mut self) -> io::Result<()> {
231			self.inner.borrow_mut().flush()
232		}
233	}
234
235	fn test<F>(expected: &str, f: F)
236	where
237		F: FnOnce(Core<TestBuf>) -> io::Result<()>,
238	{
239		let buf = TestBuf::new();
240
241		f(Core::from(buf.clone())).unwrap();
242
243		assert_eq!(buf.to_string(), expected);
244	}
245
246	#[test]
247	fn set_output() {
248		test("::set-output name=greeting::hello\n", |mut core| {
249			core.set_output("greeting", "hello")
250		});
251	}
252
253	#[test]
254	fn set_env() {
255		test("::set-env name=greeting::hello\n", |mut core| {
256			core.set_env("greeting", "hello")
257		});
258
259		assert_eq!(env::var("greeting").unwrap().as_str(), "hello");
260	}
261
262	#[test]
263	fn add_mask() {
264		test("::add-mask::super secret message\n", |mut core| {
265			core.add_mask("super secret message")
266		});
267	}
268
269	#[test]
270	fn add_path() {
271		test("::add-path::/this/is/a/test\n", |mut core| {
272			core.add_path("/this/is/a/test")
273		});
274
275		let path = env::var("PATH").unwrap();
276		let last_path = path.split(DELIMITER).last().unwrap();
277
278		assert_eq!(last_path, "/this/is/a/test");
279	}
280
281	#[test]
282	fn save_state() {
283		test("::save-state name=greeting::hello\n", |mut core| {
284			core.save_state("greeting", "hello")
285		});
286	}
287
288	#[test]
289	fn stop_logging() {
290		let buf = TestBuf::new();
291		let mut core = Core::from(buf.clone());
292		let mut token = String::new();
293
294		core.stop_logging(|| {
295			let output = buf.to_string();
296
297			assert!(output.starts_with("::stop-commands::"));
298
299			token = output.trim().split("::").last().unwrap().to_string();
300			buf.clear();
301		})
302		.unwrap();
303
304		assert_eq!(buf.to_string(), format!("::{}::\n", token));
305	}
306
307	#[test]
308	fn test_debug() {
309		test("::debug::Hello, World!\n", |mut core| {
310			core.debug("Hello, World!")
311		});
312	}
313
314	#[test]
315	fn test_error_complex() {
316		test(
317			"::error file=/test/file.rs,line=5,col=10::hello\n",
318			|mut core| {
319				core.log_error(Log {
320					message: "hello",
321					file: Some("/test/file.rs"),
322					line: Some(5),
323					col: Some(10),
324				})
325			},
326		);
327	}
328
329	#[test]
330	fn test_warning_omit() {
331		test("::warning::hello\n", |mut core| {
332			core.log_warning(Log {
333				message: "hello",
334				..Default::default()
335			})
336		});
337	}
338}