1use annotate_snippets::{
3 renderer::{AnsiColor, Style},
4 Annotation, AnnotationType, Renderer, Slice, Snippet, SourceAnnotation,
5};
6use anyhow::{bail, Context, Result};
7use forc_tracing::{println_action_green, println_error, println_red_err, println_yellow_err};
8use std::{
9 collections::{hash_map, HashSet},
10 fmt::Display,
11 fs::File,
12 hash::{Hash, Hasher},
13 path::{Path, PathBuf},
14 process::Termination,
15 str,
16};
17use sway_core::language::parsed::TreeType;
18use sway_error::{
19 diagnostic::{Diagnostic, Issue, Label, LabelType, Level, ToDiagnostic},
20 error::CompileError,
21 warning::CompileWarning,
22};
23use sway_types::{LineCol, LineColRange, SourceEngine, Span};
24use sway_utils::constants;
25
26pub mod bytecode;
27pub mod fs_locking;
28pub mod restricted;
29
30#[macro_use]
31pub mod cli;
32
33pub use ansiterm;
34pub use paste;
35pub use regex::Regex;
36pub use serial_test;
37
38pub const DEFAULT_OUTPUT_DIRECTORY: &str = "out";
39pub const DEFAULT_ERROR_EXIT_CODE: u8 = 1;
40pub const DEFAULT_SUCCESS_EXIT_CODE: u8 = 0;
41
42pub type ForcResult<T, E = ForcError> = Result<T, E>;
45
46#[derive(Debug)]
49pub struct ForcCliResult<T> {
50 result: ForcResult<T>,
51}
52
53#[derive(Debug)]
56pub struct ForcError {
57 error: anyhow::Error,
58 exit_code: u8,
59}
60
61impl ForcError {
62 pub fn new(error: anyhow::Error, exit_code: u8) -> Self {
63 Self { error, exit_code }
64 }
65
66 pub fn exit_code(self, exit_code: u8) -> Self {
68 Self {
69 error: self.error,
70 exit_code,
71 }
72 }
73}
74
75impl AsRef<anyhow::Error> for ForcError {
76 fn as_ref(&self) -> &anyhow::Error {
77 &self.error
78 }
79}
80
81impl From<&str> for ForcError {
82 fn from(value: &str) -> Self {
83 Self {
84 error: anyhow::anyhow!("{value}"),
85 exit_code: DEFAULT_ERROR_EXIT_CODE,
86 }
87 }
88}
89
90impl From<anyhow::Error> for ForcError {
91 fn from(value: anyhow::Error) -> Self {
92 Self {
93 error: value,
94 exit_code: DEFAULT_ERROR_EXIT_CODE,
95 }
96 }
97}
98
99impl From<std::io::Error> for ForcError {
100 fn from(value: std::io::Error) -> Self {
101 Self {
102 error: value.into(),
103 exit_code: DEFAULT_ERROR_EXIT_CODE,
104 }
105 }
106}
107
108impl Display for ForcError {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 self.error.fmt(f)
111 }
112}
113
114impl<T> Termination for ForcCliResult<T> {
115 fn report(self) -> std::process::ExitCode {
116 match self.result {
117 Ok(_) => DEFAULT_SUCCESS_EXIT_CODE.into(),
118 Err(e) => {
119 println_error(&format!("{}", e));
120 e.exit_code.into()
121 }
122 }
123 }
124}
125
126impl<T> From<ForcResult<T>> for ForcCliResult<T> {
127 fn from(value: ForcResult<T>) -> Self {
128 Self { result: value }
129 }
130}
131
132#[macro_export]
133macro_rules! forc_result_bail {
134 ($msg:literal $(,)?) => {
135 return $crate::ForcResult::Err(anyhow::anyhow!($msg).into())
136 };
137 ($err:expr $(,)?) => {
138 return $crate::ForcResult::Err(anyhow::anyhow!($err).into())
139 };
140 ($fmt:expr, $($arg:tt)*) => {
141 return $crate::ForcResult::Err(anyhow::anyhow!($fmt, $($arg)*).into())
142 };
143}
144
145#[cfg(feature = "tx")]
146pub mod tx_utils {
147
148 use anyhow::Result;
149 use clap::Args;
150 use fuels_core::{codec::ABIDecoder, types::param_types::ParamType};
151 use serde::{Deserialize, Serialize};
152 use std::collections::HashMap;
153 use sway_core::{asm_generation::ProgramABI, fuel_prelude::fuel_tx};
154
155 #[derive(Debug, Args, Default, Deserialize, Serialize)]
157 pub struct Salt {
158 #[clap(long = "salt")]
163 pub salt: Option<fuel_tx::Salt>,
164 }
165
166 pub fn format_log_receipts(
168 receipts: &[fuel_tx::Receipt],
169 pretty_print: bool,
170 ) -> Result<String> {
171 let mut receipt_to_json_array = serde_json::to_value(receipts)?;
172 for (rec_index, receipt) in receipts.iter().enumerate() {
173 let rec_value = receipt_to_json_array.get_mut(rec_index).ok_or_else(|| {
174 anyhow::anyhow!(
175 "Serialized receipts does not contain {} th index",
176 rec_index
177 )
178 })?;
179 match receipt {
180 fuel_tx::Receipt::LogData {
181 data: Some(data), ..
182 } => {
183 if let Some(v) = rec_value.pointer_mut("/LogData/data") {
184 *v = hex::encode(data).into();
185 }
186 }
187 fuel_tx::Receipt::ReturnData {
188 data: Some(data), ..
189 } => {
190 if let Some(v) = rec_value.pointer_mut("/ReturnData/data") {
191 *v = hex::encode(data).into();
192 }
193 }
194 _ => {}
195 }
196 }
197 if pretty_print {
198 Ok(serde_json::to_string_pretty(&receipt_to_json_array)?)
199 } else {
200 Ok(serde_json::to_string(&receipt_to_json_array)?)
201 }
202 }
203
204 pub struct DecodedLog {
206 pub value: String,
207 }
208
209 pub fn decode_log_data(
210 log_id: &str,
211 log_data: &[u8],
212 program_abi: &ProgramABI,
213 ) -> anyhow::Result<DecodedLog> {
214 let program_abi = match program_abi {
215 ProgramABI::Fuel(fuel_abi) => Some(
216 fuel_abi_types::abi::unified_program::UnifiedProgramABI::from_counterpart(
217 fuel_abi,
218 )?,
219 ),
220 _ => None,
221 }
222 .ok_or_else(|| anyhow::anyhow!("only fuelvm is supported for log decoding"))?;
223 let type_lookup = program_abi
225 .types
226 .iter()
227 .map(|decl| (decl.type_id, decl.clone()))
228 .collect::<HashMap<_, _>>();
229
230 let logged_type_lookup: HashMap<_, _> = program_abi
231 .logged_types
232 .iter()
233 .flatten()
234 .map(|logged_type| (logged_type.log_id.as_str(), logged_type.application.clone()))
235 .collect();
236
237 let type_application = logged_type_lookup
238 .get(&log_id)
239 .ok_or_else(|| anyhow::anyhow!("log id is missing"))?;
240
241 let abi_decoder = ABIDecoder::default();
242 let param_type = ParamType::try_from_type_application(type_application, &type_lookup)?;
243 let decoded_str = abi_decoder.decode_as_debug_str(¶m_type, log_data)?;
244 let decoded_log = DecodedLog { value: decoded_str };
245
246 Ok(decoded_log)
247 }
248}
249
250pub fn find_file_name<'sc>(manifest_dir: &Path, entry_path: &'sc Path) -> Result<&'sc Path> {
251 let mut file_path = manifest_dir.to_path_buf();
252 file_path.pop();
253 let file_name = match entry_path.strip_prefix(file_path.clone()) {
254 Ok(o) => o,
255 Err(err) => bail!(err),
256 };
257 Ok(file_name)
258}
259
260pub fn lock_path(manifest_dir: &Path) -> PathBuf {
261 manifest_dir.join(constants::LOCK_FILE_NAME)
262}
263
264pub fn validate_project_name(name: &str) -> Result<()> {
265 restricted::is_valid_project_name_format(name)?;
266 validate_name(name, "project name")
267}
268
269pub fn validate_name(name: &str, use_case: &str) -> Result<()> {
273 restricted::contains_invalid_char(name, use_case)?;
275
276 if restricted::is_keyword(name) {
277 bail!("the name `{name}` cannot be used as a {use_case}, it is a Sway keyword");
278 }
279 if restricted::is_conflicting_artifact_name(name) {
280 bail!(
281 "the name `{name}` cannot be used as a {use_case}, \
282 it conflicts with Forc's build directory names"
283 );
284 }
285 if name.to_lowercase() == "test" {
286 bail!(
287 "the name `test` cannot be used as a {use_case}, \
288 it conflicts with Sway's built-in test library"
289 );
290 }
291 if restricted::is_conflicting_suffix(name) {
292 bail!(
293 "the name `{name}` is part of Sway's standard library\n\
294 It is recommended to use a different name to avoid problems."
295 );
296 }
297 if restricted::is_windows_reserved(name) {
298 if cfg!(windows) {
299 bail!("cannot use name `{name}`, it is a reserved Windows filename");
300 } else {
301 bail!(
302 "the name `{name}` is a reserved Windows filename\n\
303 This package will not work on Windows platforms."
304 );
305 }
306 }
307 if restricted::is_non_ascii_name(name) {
308 bail!("the name `{name}` contains non-ASCII characters which are unsupported");
309 }
310 Ok(())
311}
312
313pub fn kebab_to_snake_case(s: &str) -> String {
315 s.replace('-', "_")
316}
317
318pub fn default_output_directory(manifest_dir: &Path) -> PathBuf {
319 manifest_dir.join(DEFAULT_OUTPUT_DIRECTORY)
320}
321
322pub fn user_forc_directory() -> PathBuf {
324 dirs::home_dir()
325 .expect("unable to find the user home directory")
326 .join(constants::USER_FORC_DIRECTORY)
327}
328
329pub fn git_checkouts_directory() -> PathBuf {
331 user_forc_directory().join("git").join("checkouts")
332}
333
334fn fd_lock_path<X: AsRef<Path>>(path: X) -> PathBuf {
344 const LOCKS_DIR_NAME: &str = ".locks";
345 const LOCK_EXT: &str = "forc-lock";
346 let file_name = hash_path(path);
347 user_forc_directory()
348 .join(LOCKS_DIR_NAME)
349 .join(file_name)
350 .with_extension(LOCK_EXT)
351}
352
353fn hash_path<X: AsRef<Path>>(path: X) -> String {
356 let path = path.as_ref();
357 let mut hasher = hash_map::DefaultHasher::default();
358 path.hash(&mut hasher);
359 let hash = hasher.finish();
360 let file_name = match path.file_stem().and_then(|s| s.to_str()) {
361 None => format!("{hash:X}"),
362 Some(stem) => format!("{hash:X}-{stem}"),
363 };
364 file_name
365}
366
367pub fn path_lock<X: AsRef<Path>>(path: X) -> Result<fd_lock::RwLock<File>> {
371 let lock_path = fd_lock_path(path);
372 let lock_dir = lock_path
373 .parent()
374 .expect("lock path has no parent directory");
375 std::fs::create_dir_all(lock_dir).context("failed to create forc advisory lock directory")?;
376 let lock_file = File::create(&lock_path).context("failed to create advisory lock file")?;
377 Ok(fd_lock::RwLock::new(lock_file))
378}
379
380pub fn program_type_str(ty: &TreeType) -> &'static str {
381 match ty {
382 TreeType::Script => "script",
383 TreeType::Contract => "contract",
384 TreeType::Predicate => "predicate",
385 TreeType::Library => "library",
386 }
387}
388
389pub fn print_compiling(ty: Option<&TreeType>, name: &str, src: &dyn std::fmt::Display) {
390 let ty = match ty {
393 Some(ty) => format!("{} ", program_type_str(ty)),
394 None => "".to_string(),
395 };
396 println_action_green(
397 "Compiling",
398 &format!("{ty}{} ({src})", ansiterm::Style::new().bold().paint(name)),
399 );
400}
401
402pub fn print_warnings(
403 source_engine: &SourceEngine,
404 terse_mode: bool,
405 proj_name: &str,
406 warnings: &[CompileWarning],
407 tree_type: &TreeType,
408) {
409 if warnings.is_empty() {
410 return;
411 }
412 let type_str = program_type_str(tree_type);
413
414 if !terse_mode {
415 warnings
416 .iter()
417 .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
418 }
419
420 println_yellow_err(&format!(
421 " Compiled {} {:?} with {} {}.",
422 type_str,
423 proj_name,
424 warnings.len(),
425 if warnings.len() > 1 {
426 "warnings"
427 } else {
428 "warning"
429 }
430 ));
431}
432
433pub fn print_on_failure(
434 source_engine: &SourceEngine,
435 terse_mode: bool,
436 warnings: &[CompileWarning],
437 errors: &[CompileError],
438 reverse_results: bool,
439) {
440 let e_len = errors.len();
441 let w_len = warnings.len();
442
443 if !terse_mode {
444 if reverse_results {
445 warnings
446 .iter()
447 .rev()
448 .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
449 errors
450 .iter()
451 .rev()
452 .for_each(|e| format_diagnostic(&e.to_diagnostic(source_engine)));
453 } else {
454 warnings
455 .iter()
456 .for_each(|w| format_diagnostic(&w.to_diagnostic(source_engine)));
457 errors
458 .iter()
459 .for_each(|e| format_diagnostic(&e.to_diagnostic(source_engine)));
460 }
461 }
462
463 if e_len == 0 && w_len > 0 {
464 println_red_err(&format!(
465 " Aborting. {} warning(s) treated as error(s).",
466 warnings.len()
467 ));
468 } else {
469 println_red_err(&format!(
470 " Aborting due to {} {}.",
471 e_len,
472 if e_len > 1 { "errors" } else { "error" }
473 ));
474 }
475}
476
477pub fn create_diagnostics_renderer() -> Renderer {
482 Renderer::styled()
486 .warning(
487 Style::new()
488 .bold()
489 .fg_color(Some(AnsiColor::BrightYellow.into())),
490 )
491 .error(
492 Style::new()
493 .bold()
494 .fg_color(Some(AnsiColor::BrightRed.into())),
495 )
496}
497
498pub fn format_diagnostic(diagnostic: &Diagnostic) {
499 const SHOW_DIAGNOSTIC_CODE: bool = false;
502
503 if diagnostic.is_old_style() {
504 format_old_style_diagnostic(diagnostic.issue());
505 return;
506 }
507
508 let mut label = String::new();
509 get_title_label(diagnostic, &mut label);
510
511 let snippet_title = Some(Annotation {
512 label: Some(label.as_str()),
513 id: if SHOW_DIAGNOSTIC_CODE {
514 diagnostic.reason().map(|reason| reason.code())
515 } else {
516 None
517 },
518 annotation_type: diagnostic_level_to_annotation_type(diagnostic.level()),
519 });
520
521 let mut snippet_slices = Vec::<Slice<'_>>::new();
522
523 if diagnostic.issue().is_in_source() {
525 snippet_slices.push(construct_slice(diagnostic.labels_in_issue_source()))
526 }
527
528 for source_path in diagnostic.related_sources(false) {
530 snippet_slices.push(construct_slice(diagnostic.labels_in_source(source_path)))
531 }
532
533 let mut snippet_footer = Vec::<Annotation<'_>>::new();
534 for help in diagnostic.help() {
535 snippet_footer.push(Annotation {
536 id: None,
537 label: Some(help),
538 annotation_type: AnnotationType::Help,
539 });
540 }
541
542 let snippet = Snippet {
543 title: snippet_title,
544 slices: snippet_slices,
545 footer: snippet_footer,
546 };
547
548 let renderer = create_diagnostics_renderer();
549 match diagnostic.level() {
550 Level::Info => tracing::info!("{}\n____\n", renderer.render(snippet)),
551 Level::Warning => tracing::warn!("{}\n____\n", renderer.render(snippet)),
552 Level::Error => tracing::error!("{}\n____\n", renderer.render(snippet)),
553 }
554
555 fn format_old_style_diagnostic(issue: &Issue) {
556 let annotation_type = label_type_to_annotation_type(issue.label_type());
557
558 let snippet_title = Some(Annotation {
559 label: if issue.is_in_source() {
560 None
561 } else {
562 Some(issue.text())
563 },
564 id: None,
565 annotation_type,
566 });
567
568 let mut snippet_slices = vec![];
569 if issue.is_in_source() {
570 let span = issue.span();
571 let input = span.input();
572 let mut start_pos = span.start();
573 let mut end_pos = span.end();
574 let LineColRange { mut start, end } = span.line_col_one_index();
575 let input = construct_window(&mut start, end, &mut start_pos, &mut end_pos, input);
576
577 let slice = Slice {
578 source: input,
579 line_start: start.line,
580 origin: Some(issue.source_path().unwrap().as_str()),
582 fold: false,
583 annotations: vec![SourceAnnotation {
584 label: issue.text(),
585 annotation_type,
586 range: (start_pos, end_pos),
587 }],
588 };
589
590 snippet_slices.push(slice);
591 }
592
593 let snippet = Snippet {
594 title: snippet_title,
595 footer: vec![],
596 slices: snippet_slices,
597 };
598
599 let renderer = create_diagnostics_renderer();
600 tracing::error!("{}\n____\n", renderer.render(snippet));
601 }
602
603 fn get_title_label(diagnostics: &Diagnostic, label: &mut String) {
604 label.clear();
605 if let Some(reason) = diagnostics.reason() {
606 label.push_str(reason.description());
607 }
608 }
609
610 fn diagnostic_level_to_annotation_type(level: Level) -> AnnotationType {
611 match level {
612 Level::Info => AnnotationType::Info,
613 Level::Warning => AnnotationType::Warning,
614 Level::Error => AnnotationType::Error,
615 }
616 }
617}
618
619fn construct_slice(labels: Vec<&Label>) -> Slice {
620 debug_assert!(
621 !labels.is_empty(),
622 "To construct slices, at least one label must be provided."
623 );
624
625 debug_assert!(
626 labels.iter().all(|label| label.is_in_source()),
627 "Slices can be constructed only for labels that are related to a place in source code."
628 );
629
630 debug_assert!(
631 HashSet::<&str>::from_iter(labels.iter().map(|label| label.source_path().unwrap().as_str())).len() == 1,
632 "Slices can be constructed only for labels that are related to places in the same source code."
633 );
634
635 let source_file = labels[0].source_path().map(|path| path.as_str());
636 let source_code = labels[0].span().input();
637
638 let span = Span::join_all(labels.iter().map(|label| label.span().clone()));
640
641 let (source, line_start, shift_in_bytes) = construct_code_snippet(&span, source_code);
642
643 let mut annotations = vec![];
644
645 for message in labels {
646 annotations.push(SourceAnnotation {
647 label: message.text(),
648 annotation_type: label_type_to_annotation_type(message.label_type()),
649 range: get_annotation_range(message.span(), source_code, shift_in_bytes),
650 });
651 }
652
653 return Slice {
654 source,
655 line_start,
656 origin: source_file,
657 fold: true,
658 annotations,
659 };
660
661 fn get_annotation_range(
662 span: &Span,
663 source_code: &str,
664 shift_in_bytes: usize,
665 ) -> (usize, usize) {
666 let mut start_pos = span.start();
667 let mut end_pos = span.end();
668
669 let start_ix_bytes = start_pos - std::cmp::min(shift_in_bytes, start_pos);
670 let end_ix_bytes = end_pos - std::cmp::min(shift_in_bytes, end_pos);
671
672 start_pos = source_code[shift_in_bytes..(shift_in_bytes + start_ix_bytes)]
674 .chars()
675 .count();
676 end_pos = source_code[shift_in_bytes..(shift_in_bytes + end_ix_bytes)]
677 .chars()
678 .count();
679
680 (start_pos, end_pos)
681 }
682}
683
684fn label_type_to_annotation_type(label_type: LabelType) -> AnnotationType {
685 match label_type {
686 LabelType::Info => AnnotationType::Info,
687 LabelType::Help => AnnotationType::Help,
688 LabelType::Warning => AnnotationType::Warning,
689 LabelType::Error => AnnotationType::Error,
690 }
691}
692
693fn construct_code_snippet<'a>(span: &Span, input: &'a str) -> (&'a str, usize, usize) {
702 const NUM_LINES_BUFFER: usize = 2;
704
705 let LineColRange { start, end } = span.line_col_one_index();
706
707 let total_lines_in_input = input.chars().filter(|x| *x == '\n').count();
708 debug_assert!(end.line >= start.line);
709 let total_lines_of_highlight = end.line - start.line;
710 debug_assert!(total_lines_in_input >= total_lines_of_highlight);
711
712 let mut current_line = 0;
713 let mut lines_to_start_of_snippet = 0;
714 let mut calculated_start_ix = None;
715 let mut calculated_end_ix = None;
716 let mut pos = 0;
717 for character in input.chars() {
718 if character == '\n' {
719 current_line += 1
720 }
721
722 if current_line + NUM_LINES_BUFFER >= start.line && calculated_start_ix.is_none() {
723 calculated_start_ix = Some(pos);
724 lines_to_start_of_snippet = current_line;
725 }
726
727 if current_line >= end.line + NUM_LINES_BUFFER && calculated_end_ix.is_none() {
728 calculated_end_ix = Some(pos);
729 }
730
731 if calculated_start_ix.is_some() && calculated_end_ix.is_some() {
732 break;
733 }
734 pos += character.len_utf8();
735 }
736 let calculated_start_ix = calculated_start_ix.unwrap_or(0);
737 let calculated_end_ix = calculated_end_ix.unwrap_or(input.len());
738
739 (
740 &input[calculated_start_ix..calculated_end_ix],
741 lines_to_start_of_snippet,
742 calculated_start_ix,
743 )
744}
745
746fn construct_window<'a>(
755 start: &mut LineCol,
756 end: LineCol,
757 start_ix: &mut usize,
758 end_ix: &mut usize,
759 input: &'a str,
760) -> &'a str {
761 const NUM_LINES_BUFFER: usize = 2;
763
764 let total_lines_in_input = input.chars().filter(|x| *x == '\n').count();
765 debug_assert!(end.line >= start.line);
766 let total_lines_of_highlight = end.line - start.line;
767 debug_assert!(total_lines_in_input >= total_lines_of_highlight);
768
769 let mut current_line = 1usize;
770
771 let mut chars = input.char_indices().map(|(char_offset, character)| {
772 let r = (current_line, char_offset);
773 if character == '\n' {
774 current_line += 1;
775 }
776 r
777 });
778
779 let first_char = chars
781 .by_ref()
782 .find(|(current_line, _)| current_line + NUM_LINES_BUFFER >= start.line);
783
784 let last_char = chars
786 .by_ref()
787 .find(|(current_line, _)| *current_line > end.line + NUM_LINES_BUFFER)
788 .map(|x| x.1);
789
790 drop(chars);
792
793 let (first_char_line, first_char_offset, last_char_offset) = match (first_char, last_char) {
794 (Some((first_char_line, first_char_offset)), Some(last_char_offset)) => {
796 (first_char_line, first_char_offset, last_char_offset)
797 }
798 (Some((first_char_line, first_char_offset)), None) => {
800 (first_char_line, first_char_offset, input.len())
801 }
802 _ => (current_line, input.len(), input.len()),
804 };
805
806 start.line = first_char_line;
808 *start_ix = start_ix.saturating_sub(first_char_offset);
809 *end_ix = end_ix.saturating_sub(first_char_offset);
810
811 &input[first_char_offset..last_char_offset]
812}
813
814#[test]
815fn ok_construct_window() {
816 fn t(
817 start_line: usize,
818 start_col: usize,
819 end_line: usize,
820 end_col: usize,
821 start_char: usize,
822 end_char: usize,
823 input: &str,
824 ) -> (usize, usize, &str) {
825 let mut s = LineCol {
826 line: start_line,
827 col: start_col,
828 };
829 let mut start = start_char;
830 let mut end = end_char;
831 let r = construct_window(
832 &mut s,
833 LineCol {
834 line: end_line,
835 col: end_col,
836 },
837 &mut start,
838 &mut end,
839 input,
840 );
841 (start, end, r)
842 }
843
844 assert_eq!(t(0, 0, 0, 0, 0, 0, ""), (0, 0, ""));
846
847 assert_eq!(t(1, 1, 1, 1, 0, 0, ""), (0, 0, ""));
849
850 assert_eq!(t(1, 7, 1, 7, 6, 6, "script"), (6, 6, "script"));
852
853 let eight_lines = "1\n2\n3\n4\n5\n6\n7\n8";
855
856 assert_eq!(t(1, 1, 1, 1, 0, 1, eight_lines), (0, 1, "1\n2\n3\n"));
857 assert_eq!(t(2, 1, 2, 1, 2, 3, eight_lines), (2, 3, "1\n2\n3\n4\n"));
858 assert_eq!(t(3, 1, 3, 1, 4, 5, eight_lines), (4, 5, "1\n2\n3\n4\n5\n"));
859 assert_eq!(t(4, 1, 4, 1, 6, 7, eight_lines), (4, 5, "2\n3\n4\n5\n6\n"));
860 assert_eq!(t(5, 1, 5, 1, 8, 9, eight_lines), (4, 5, "3\n4\n5\n6\n7\n"));
861 assert_eq!(t(6, 1, 6, 1, 10, 11, eight_lines), (4, 5, "4\n5\n6\n7\n8"));
862 assert_eq!(t(7, 1, 7, 1, 12, 13, eight_lines), (4, 5, "5\n6\n7\n8"));
863 assert_eq!(t(8, 1, 8, 1, 14, 15, eight_lines), (4, 5, "6\n7\n8"));
864
865 assert_eq!(t(9, 1, 9, 1, 14, 15, eight_lines), (2, 3, "7\n8"));
867 assert_eq!(t(10, 1, 10, 1, 14, 15, eight_lines), (0, 1, "8"));
868 assert_eq!(t(11, 1, 11, 1, 14, 15, eight_lines), (0, 0, ""));
869}