roc/crates/repl_test/src/state.rs
2023-09-19 09:32:52 +02:00

212 lines
6.5 KiB
Rust

use bumpalo::Bump;
use indoc::indoc;
use roc_repl_cli::{evaluate, ReplHelper};
use roc_repl_ui::is_incomplete;
use roc_repl_ui::repl_state::{ReplAction, ReplState};
use roc_reporting::report::DEFAULT_PALETTE;
use roc_target::TargetInfo;
use rustyline::Editor;
use target_lexicon::Triple;
// These are tests of the REPL state machine. They work without actually
// running the CLI, and without using rustyline, and instead verify
// the expected outputs for various sequences of user input strings.
#[test]
fn one_plus_one() {
complete("1 + 1", &mut ReplState::new(), "2 : Num *");
}
#[test]
fn persisted_defs() {
let mut state = ReplState::new();
complete("x = 5", &mut state, "5 : Num *");
complete("7 - 3", &mut state, "4 : Num *");
complete("y = 6", &mut state, "6 : Num *");
}
#[test]
fn annotated_body() {
let mut input = "t : [A, B, C]".to_string();
incomplete(&mut input);
input.push_str("t = A");
complete(&input, &mut ReplState::new(), "A : [A, B, C]");
}
#[test]
fn exhaustiveness_problem() {
let mut state = ReplState::new();
// Enter an annotated tag union to make it exhaustive
{
let mut input = "t : [A, B, C]".to_string();
incomplete(&mut input);
input.push_str("t = A");
complete(&input, &mut state, "A : [A, B, C]");
}
// Run a `when` on it that isn't exhaustive
{
let mut input = "when t is".to_string();
incomplete(&mut input);
input.push_str(" A -> 1");
incomplete(&mut input);
let expected_error: &str = indoc!(
r#"
── UNSAFE PATTERN ──────────────────────────────────────────────────────────────
This when does not cover all the possibilities:
7│> when t is
8│> A -> 1
Other possibilities include:
B
C
I would have to crash if I saw one of those! Add branches for them!"#
);
error(&input, &mut state, expected_error.to_string());
}
}
#[test]
fn partial_record_definition() {
// Partially define a record successfully
{
let mut state = ReplState::new();
let mut input = "successfulRecord = {".to_string();
incomplete(&mut input);
input.push_str("field: \"field\",");
incomplete(&mut input);
input.push('}');
complete(&input, &mut state, "{ field: \"field\" } : { field : Str }");
}
// Partially define a record incompletely
{
let mut state = ReplState::new();
let mut input = "failedRecord = {".to_string();
incomplete(&mut input);
input.push_str("field: \"field\",");
incomplete(&mut input);
input.push('\n');
let expected_error: &str = indoc!(
r#"
── RECORD PARSE PROBLEM ────────────────────────────────────────────────────────
I am partway through parsing a record, but I got stuck here:
1│ app "app" provides [replOutput] to "./platform"
2│
3│ replOutput =
4│ failedRecord = {
^
TODO provide more context."#
);
error(&input, &mut state, expected_error.to_string());
}
}
#[test]
fn tips() {
assert!(!is_incomplete(""));
let arena = Bump::new();
let target = Triple::host();
let target_info = TargetInfo::from(&target);
let action = ReplState::default().step(&arena, "", target_info, DEFAULT_PALETTE);
assert!(matches!(action, ReplAction::Help));
}
#[test]
fn standalone_annotation() {
let mut state = ReplState::new();
let mut input = "x : Str".to_string();
incomplete(&mut input);
assert!(!is_incomplete(&input));
let arena = Bump::new();
let target = Triple::host();
let target_info = TargetInfo::from(&target);
let action = state.step(&arena, &input, target_info, DEFAULT_PALETTE);
assert!(matches!(action, ReplAction::Nothing));
}
/// validate and step the given input, then check the Result vs the output
/// with ANSI escape codes stripped.
fn complete(input: &str, state: &mut ReplState, expected_start: &str) {
assert!(!is_incomplete(input));
let arena = Bump::new();
let target = Triple::host();
let target_info = TargetInfo::from(&target);
let action = state.step(&arena, input, target_info, DEFAULT_PALETTE);
let repl_helper = ReplHelper::default();
let mut editor = Editor::<ReplHelper>::new();
editor.set_helper(Some(repl_helper));
match action {
ReplAction::Eval { opt_mono, problems } => {
let string = evaluate(opt_mono, problems, &target);
let escaped =
std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap())
.unwrap();
let comment_index = escaped.rfind('#').unwrap_or(escaped.len());
assert_eq!(expected_start, (escaped[0..comment_index].trim()));
}
_ => {
panic!("Unexpected action: {:?}", action);
}
}
}
fn incomplete(input: &mut String) {
assert!(is_incomplete(input));
// Since this was incomplete, rustyline won't step the state. Instead, it will
// remember the input (with a newline appended) for next time.
input.push('\n');
}
/// validate and step the given input, then check the given string vs the output
/// with ANSI escape codes stripped.
fn error(input: &str, state: &mut ReplState, expected_step_result: String) {
assert!(!is_incomplete(input));
let arena = Bump::new();
let target = Triple::host();
let target_info = TargetInfo::from(&target);
let action = state.step(&arena, input, target_info, DEFAULT_PALETTE);
let repl_helper = ReplHelper::default();
let mut editor = Editor::<ReplHelper>::new();
editor.set_helper(Some(repl_helper));
match action {
ReplAction::Eval { opt_mono, problems } => {
let string = evaluate(opt_mono, problems, &target);
let escaped =
std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap())
.unwrap();
assert_eq!(expected_step_result, escaped);
}
_ => {
panic!("Unexpected action: {:?}", action);
}
}
}