mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-26 13:29:12 +00:00
merge upstream/main
This commit is contained in:
commit
cec67721e6
59 changed files with 2542 additions and 990 deletions
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ extern crate lazy_static;
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod state;
|
||||
|
||||
#[cfg(all(test, not(feature = "wasm")))]
|
||||
mod cli;
|
||||
|
||||
|
|
152
crates/repl_test/src/state.rs
Normal file
152
crates/repl_test/src/state.rs
Normal 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);
|
||||
}
|
|
@ -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"#,
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue