merge upstream/main

This commit is contained in:
Luke Boswell 2022-11-06 09:27:46 +11:00
commit cec67721e6
No known key found for this signature in database
GPG key ID: 0E908525B2C7BD68
59 changed files with 2542 additions and 990 deletions

View file

@ -3,16 +3,16 @@ use std::io::Write;
use std::path::PathBuf;
use std::process::{Command, ExitStatus, Stdio};
use roc_repl_cli::{TIPS, WELCOME_MESSAGE};
use roc_repl_cli::{SHORT_INSTRUCTIONS, WELCOME_MESSAGE};
use roc_test_utils::assert_multiline_str_eq;
const ERROR_MESSAGE_START: char = '─';
#[derive(Debug)]
struct Out {
stdout: String,
stderr: String,
status: ExitStatus,
pub struct Out {
pub stdout: String,
pub stderr: String,
pub status: ExitStatus,
}
fn path_to_roc_binary() -> PathBuf {
@ -39,7 +39,7 @@ fn path_to_roc_binary() -> PathBuf {
path
}
fn repl_eval(input: &str) -> Out {
pub fn repl_eval(input: &str) -> Out {
let mut cmd = Command::new(path_to_roc_binary());
cmd.arg("repl");
@ -75,7 +75,7 @@ fn repl_eval(input: &str) -> Out {
// Remove the initial instructions from the output.
let expected_instructions = format!("{}{}", WELCOME_MESSAGE, TIPS);
let expected_instructions = format!("{}{}", WELCOME_MESSAGE, SHORT_INSTRUCTIONS);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(
@ -124,10 +124,25 @@ fn repl_eval(input: &str) -> Out {
}
pub fn expect_success(input: &str, expected: &str) {
let out = repl_eval(input);
let out = repl_eval(input.trim());
assert_multiline_str_eq!("", out.stderr.as_str());
assert_multiline_str_eq!(expected, out.stdout.as_str());
// Don't consider the auto variable name (e.g. "# val1") at the end.
// The state.rs tests do that!
let mut iter = out.stdout.lines().rev();
let line = iter.next().unwrap();
let comment_index = line.rfind('#').unwrap_or(line.len());
let line_without_comment = line[0..comment_index].trim_end();
// Sometimes the "# val1" wraps around to its own line; if this happens,
// we just use the preceding line instead.
if line_without_comment.is_empty() {
assert_multiline_str_eq!(expected, iter.next().unwrap().trim_end());
} else {
assert_multiline_str_eq!(expected, line_without_comment);
}
assert!(out.status.success());
}

View file

@ -6,6 +6,9 @@ extern crate lazy_static;
#[cfg(test)]
mod tests;
#[cfg(test)]
mod state;
#[cfg(all(test, not(feature = "wasm")))]
mod cli;

View file

@ -0,0 +1,152 @@
use indoc::indoc;
use roc_repl_cli::repl_state::{is_incomplete, ReplState, TIPS};
// 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(), Ok(("2 : Num *", "val1")));
}
#[test]
fn generated_expr_names() {
let mut state = ReplState::new();
complete("2 * 3", &mut state, Ok(("6 : Num *", "val1")));
complete("4 - 1", &mut state, Ok(("3 : Num *", "val2")));
complete("val1 + val2", &mut state, Ok(("9 : Num *", "val3")));
complete("1 + (val2 * val3)", &mut state, Ok(("28 : Num *", "val4")));
}
#[test]
fn persisted_defs() {
let mut state = ReplState::new();
complete("x = 5", &mut state, Ok(("5 : Num *", "x")));
complete("7 - 3", &mut state, Ok(("4 : Num *", "val1")));
complete("y = 6", &mut state, Ok(("6 : Num *", "y")));
complete("val1 + x + y", &mut state, Ok(("15 : Num *", "val2")));
}
#[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(), Ok(("A : [A, B, C]", "t")));
}
#[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, Ok(("A : [A, B, C]", "t")));
}
// 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);
const 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 tips() {
assert!(!is_incomplete(""));
assert_eq!(ReplState::new().step("", None), Ok(TIPS.to_string()));
}
#[test]
fn standalone_annotation() {
let mut state = ReplState::new();
let mut input = "x : Str".to_string();
incomplete(&mut input);
assert!(!is_incomplete(&input));
assert_eq!(state.step(&input, None), Ok(String::new()));
}
/// 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_step_result: Result<(&str, &str), i32>) {
assert!(!is_incomplete(input));
match state.step(input, None) {
Ok(string) => {
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_step_result.map(|(starts_with, _)| starts_with),
Ok(escaped[0..comment_index].trim())
);
assert_eq!(
expected_step_result.map(|(_, ends_with)| ends_with),
// +1 because we want to skip over the '#' itself
Ok(escaped[comment_index + 1..].trim())
);
}
Err(err) => {
assert_eq!(expected_step_result, Err(err));
}
}
}
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 escaped = state.step(input, None).map(|string| {
std::string::String::from_utf8(strip_ansi_escapes::strip(string.trim()).unwrap()).unwrap()
});
assert_eq!(Ok(expected_step_result), escaped);
}

View file

@ -1,8 +1,9 @@
#[allow(unused_imports)]
use indoc::indoc;
use roc_test_utils::assert_multiline_str_eq;
#[cfg(not(feature = "wasm"))]
use crate::cli::{expect_failure, expect_success};
use crate::cli::{expect_failure, expect_success, repl_eval};
#[cfg(feature = "wasm")]
#[allow(unused_imports)]
@ -199,8 +200,7 @@ fn newtype_of_big_data() {
Either a b : [Left a, Right b]
lefty : Either Str Str
lefty = Left "loosey"
A lefty
"#
A lefty"#
),
r#"A (Left "loosey") : [A (Either Str Str)]"#,
)
@ -214,8 +214,7 @@ fn newtype_nested() {
Either a b : [Left a, Right b]
lefty : Either Str Str
lefty = Left "loosey"
A (B (C lefty))
"#
A (B (C lefty))"#
),
r#"A (B (C (Left "loosey"))) : [A [B [C (Either Str Str)]]]"#,
)
@ -229,8 +228,7 @@ fn newtype_of_big_of_newtype() {
Big a : [Big a [Wrapper [Newtype a]]]
big : Big Str
big = Big "s" (Wrapper (Newtype "t"))
A big
"#
A big"#
),
r#"A (Big "s" (Wrapper (Newtype "t"))) : [A (Big Str)]"#,
)
@ -560,17 +558,45 @@ fn four_element_record() {
);
}
#[cfg(not(feature = "wasm"))]
#[test]
fn multiline_string() {
fn multiline_string_non_wasm() {
// If a string contains newlines, format it as a multiline string in the output.
// We can't use expect_success to test this, because it only looks at the last
// line of output, and in this case we care about every line of output!
let out = repl_eval(r#""\n\nhi!\n\n""#);
let expected = indoc!(
r#""""
hi!
""" : Str"#
);
assert_multiline_str_eq!("", out.stderr.as_str());
// Don't consider the auto variable name ("# val1") at the end.
// The state.rs tests do that!
assert_multiline_str_eq!(expected, out.stdout.replace("# val1", "").trim());
assert!(out.status.success());
}
#[cfg(feature = "wasm")]
#[test]
fn multiline_string_wasm() {
// If a string contains newlines, format it as a multiline string in the output
expect_success(
r#""\n\nhi!\n\n""#,
indoc!(
r#""""
hi!
""" : Str"#
),
@ -933,39 +959,7 @@ fn parse_problem() {
);
}
#[cfg(not(feature = "wasm"))] // TODO: mismatch is due to terminal control codes!
#[test]
fn exhaustiveness_problem() {
expect_failure(
indoc!(
r#"
t : [A, B, C]
t = A
when t is
A -> "a"
"#
),
indoc!(
r#"
UNSAFE PATTERN
This when does not cover all the possibilities:
7> when t is
8> A -> "a"
Other possibilities include:
B
C
I would have to crash if I saw one of those! Add branches for them!
"#
),
);
}
#[ignore] // re-enable (and fix) after https://github.com/roc-lang/roc/issues/4425 is done!
#[cfg(not(feature = "wasm"))]
#[test]
fn issue_2343_complete_mono_with_shadowed_vars() {
@ -1024,8 +1018,7 @@ fn tag_with_type_behind_alias() {
T : [A Str]
v : T
v = A "value"
v
"#
v"#
),
r#"A "value" : T"#,
);
@ -1039,8 +1032,7 @@ fn issue_2588_record_with_function_and_nonfunction() {
r#"
x = 1
f = \n -> n * 2
{ y: f x, f }
"#
{ y: f x, f }"#
),
r#"{ f: <function>, y: 2 } : { f : Num a -> Num a, y : Num * }"#,
)
@ -1053,8 +1045,7 @@ fn opaque_apply() {
r#"
Age := U32
@Age 23
"#
@Age 23"#
),
"@Age 23 : Age",
)
@ -1067,8 +1058,7 @@ fn opaque_apply_polymorphic() {
r#"
F t u := [Package t u]
@F (Package "" { a: "" })
"#
@F (Package "" { a: "" })"#
),
r#"@F (Package "" { a: "" }) : F Str { a : Str }"#,
)
@ -1083,8 +1073,7 @@ fn opaque_pattern_and_call() {
f = \@F (Package A {}) -> @F (Package {} A)
f (@F (Package A {}))
"#
f (@F (Package A {}))"#
),
r#"@F (Package {} A) : F {} [A]"#,
)
@ -1097,10 +1086,9 @@ fn dec_in_repl() {
r#"
x: Dec
x=1.23
x
"#
x"#
),
r#"1.23 : Dec"#,
"1.23 : Dec",
)
}
@ -1111,8 +1099,7 @@ fn print_i8_issue_2710() {
r#"
a : I8
a = -1
a
"#
a"#
),
r#"-1 : I8"#,
)
@ -1124,8 +1111,7 @@ fn box_box() {
expect_success(
indoc!(
r#"
Box.box "container store"
"#
Box.box "container store""#
),
r#"Box.box "container store" : Box Str"#,
)
@ -1140,8 +1126,7 @@ fn box_box_type_alias() {
HeapStr : Box Str
helloHeap : HeapStr
helloHeap = Box.box "bye stacks"
helloHeap
"#
helloHeap"#
),
r#"Box.box "bye stacks" : HeapStr"#,
)
@ -1165,9 +1150,7 @@ fn issue_2818() {
f : {} -> List Str
f = \_ ->
x = []
x
f
"#
x"#
),
r"<function> : {} -> List Str",
)
@ -1186,8 +1169,7 @@ fn issue_2810_recursive_layout_inside_nonrecursive() {
a : Job
a = Job (Command (FromJob (Job (Command SystemTool))))
a
"#
a"#
),
"Job (Command (FromJob (Job (Command SystemTool)))) : Job",
)
@ -1199,14 +1181,10 @@ fn render_nullable_unwrapped_passing_through_alias() {
indoc!(
r#"
Deep : [L DeepList]
DeepList : [Nil, Cons Deep]
v : DeepList
v = (Cons (L (Cons (L (Cons (L Nil))))))
v
"#
v"#
),
"Cons (L (Cons (L (Cons (L Nil))))) : DeepList",
)
@ -1218,8 +1196,7 @@ fn opaque_wrap_function() {
indoc!(
r#"
A a := a
List.map [1u8, 2u8, 3u8] @A
"#
List.map [1u8, 2u8, 3u8] @A"#
),
"[@A 1, @A 2, @A 3] : List (A U8)",
);
@ -1230,8 +1207,7 @@ fn dict_get_single() {
expect_success(
indoc!(
r#"
Dict.single 0 {a: 1, c: 2} |> Dict.get 0
"#
Dict.single 0 {a: 1, c: 2} |> Dict.get 0"#
),
r#"Ok { a: 1, c: 2 } : Result { a : Num *, c : Num * } [KeyNotFound]"#,
)
@ -1242,8 +1218,7 @@ fn record_of_poly_function() {
expect_success(
indoc!(
r#"
{ a: \_ -> "a" }
"#
{ a: \_ -> "a" }"#
),
r#"{ a: <function> } : { a : * -> Str }"#,
);
@ -1254,8 +1229,7 @@ fn record_of_poly_function_and_string() {
expect_success(
indoc!(
r#"
{ a: \_ -> "a", b: "b" }
"#
{ a: \_ -> "a", b: "b" }"#
),
r#"{ a: <function>, b: "b" } : { a : * -> Str, b : Str }"#,
);
@ -1266,8 +1240,7 @@ fn newtype_by_void_is_wrapped() {
expect_success(
indoc!(
r#"
Result.try (Err 42) (\x -> Err (x+1))
"#
Result.try (Err 42) (\x -> Err (x+1))"#
),
r#"Err 42 : Result b (Num *)"#,
);
@ -1275,8 +1248,7 @@ fn newtype_by_void_is_wrapped() {
expect_success(
indoc!(
r#"
Result.try (Ok 42) (\x -> Ok (x+1))
"#
Result.try (Ok 42) (\x -> Ok (x+1))"#
),
r#"Ok 43 : Result (Num *) err"#,
);